[{"data":1,"prerenderedAt":12787},["ShallowReactive",2],{"i-lucide:chevron-down":3,"i-lucide:search":8,"i-lucide:sun":10,"i-lucide:moon":12,"i-lucide:arrow-right":14,"i-lucide:menu":16,"i-lucide:mail":18,"i-simple-icons:x":20,"i-simple-icons:linkedin":22,"i-simple-icons:dribbble":25,"i-simple-icons:github":27,"sitemap-index":29,"i-lucide:chevron-right":12783,"i-lucide:lock":12785},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m6 9l6 6l6-6\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"m21 21l-4.34-4.34\"\u002F>\u003Ccircle cx=\"11\" cy=\"11\" r=\"8\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"4\"\u002F>\u003Cpath d=\"M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7l7 7l-7 7\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":17},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 5h16M4 12h16M4 19h16\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":19},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"m22 7l-8.991 5.727a2 2 0 0 1-2.009 0L2 7\"\u002F>\u003Crect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":21},"\u003Cpath fill=\"currentColor\" d=\"M14.234 10.162L22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299l-.929-1.329L3.076 1.56h3.182l5.965 8.532l.929 1.329l7.754 11.09h-3.182z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":23,"hidden":24},"\u003Cpath fill=\"currentColor\" d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037c-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85c3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.06 2.06 0 0 1-2.063-2.065a2.064 2.064 0 1 1 2.063 2.065m1.782 13.019H3.555V9h3.564zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0z\"\u002F>",true,{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":26},"\u003Cpath fill=\"currentColor\" d=\"M12 24C5.385 24 0 18.615 0 12S5.385 0 12 0s12 5.385 12 12s-5.385 12-12 12m10.12-10.358c-.35-.11-3.17-.953-6.384-.438c1.34 3.684 1.887 6.684 1.992 7.308a10.28 10.28 0 0 0 4.395-6.87zm-6.115 7.808c-.153-.9-.75-4.032-2.19-7.77l-.066.02c-5.79 2.015-7.86 6.025-8.04 6.4a10.16 10.16 0 0 0 6.29 2.166c1.42 0 2.77-.29 4-.814zm-11.62-2.58c.232-.4 3.045-5.055 8.332-6.765q.202-.067.405-.12q-.392-.879-.832-1.74C7.17 11.775 2.206 11.71 1.756 11.7l-.004.312c0 2.633.998 5.037 2.634 6.855zm-2.42-8.955c.46.008 4.683.026 9.477-1.248a66 66 0 0 0-3.8-5.928a10.28 10.28 0 0 0-5.676 7.17zM9.6 2.052c.282.38 2.145 2.914 3.822 6c3.645-1.365 5.19-3.44 5.373-3.702A10.2 10.2 0 0 0 12 1.764c-.825 0-1.63.1-2.4.285zm10.335 3.483c-.218.29-1.935 2.493-5.724 4.04c.24.49.47.985.68 1.486c.08.18.15.36.22.53c3.41-.43 6.8.26 7.14.33c-.02-2.42-.88-4.64-2.31-6.38z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":28},"\u003Cpath fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\u002F>",{"courses":30,"lessons":227,"books":3792,"articles":4366,"posts":4957,"work":9391,"legal":12365},[31,72,99,132,162,192],{"id":32,"title":33,"access":34,"body":35,"description":49,"draft":6,"estimatedMinutes":50,"extension":51,"featured":24,"icon":52,"lang":53,"level":54,"meta":55,"navigation":24,"order":56,"path":57,"prerequisites":58,"resources":59,"seo":60,"sku":61,"stem":62,"subjects":63,"summary":65,"tags":66,"updated":70,"__hash__":71},"courses\u002Flearn\u002Fbn\u002Fcourses\u002Fevent-loop\u002Findex.md","ইভেন্ট লুপ","free",{"type":36,"value":37,"toc":45},"minimark",[38,42],[39,40,41],"p",{},"এই টপিকে দুটো লেখা আছে। প্রথমটা কল স্ট্যাক নিয়ে — কোড চলার সময় ঠিক কী হয়।\nদ্বিতীয়টা কিউ নিয়ে — কাজ শেষ হওয়ার পর কলব্যাক কোথায় অপেক্ষা করে, আর কোন\nকিউ আগে সুযোগ পায়।",[39,43,44],{},"ক্রম মেনে পড়লে ভালো, কারণ দ্বিতীয়টা প্রথমটার উপর দাঁড়িয়ে আছে।",{"title":46,"searchDepth":47,"depth":47,"links":48},"",3,[],"জাভাস্ক্রিপ্ট এক থ্রেডে চলেও কীভাবে একসাথে অনেক কাজ সামলায়।",20,"md","lucide:repeat","bn","beginner",{},1,"\u002Flearn\u002Fbn\u002Fcourses\u002Fevent-loop",[],[],{"title":33,"description":49},null,"learn\u002Fbn\u002Fcourses\u002Fevent-loop\u002Findex",[64],"javascript","জাভাস্ক্রিপ্টের একটাই কল স্ট্যাক — মানে এক সময়ে একটার বেশি কাজ চলতে পারে না।\nতবু নেটওয়ার্ক রিকোয়েস্ট, টাইমার আর ক্লিক একসাথে সামলানো যায়, কারণ ভারী\nকাজগুলো ভাষাটা নিজে করে না; ব্রাউজার করে। ব্রাউজার কাজ শেষ করে কলব্যাকটা একটা\nকিউতে রেখে দেয়, আর ইভেন্ট লুপ স্ট্যাক খালি হলে সেটা তুলে নেয়।\nএই একটা নিয়ম বুঝলে `setTimeout(fn, 0)`-এর দেরি, প্রমিজের আগে চলা, আর\n\"পেজ হ্যাং করে আছে\" — তিনটাই একই ব্যাখ্যায় মিলে যায়।\n",[67,68,69],"ইভেন্ট-লুপ","অ্যাসিঙ্ক","প্রমিজ","2026-08-06","6BoixVqlBPimn2bTW-mXZLz2hVlq88kHiVNkO_SLh9w",{"id":73,"title":74,"access":34,"body":75,"description":82,"draft":6,"estimatedMinutes":83,"extension":51,"featured":24,"icon":84,"lang":53,"level":54,"meta":85,"navigation":24,"order":56,"path":86,"prerequisites":87,"resources":88,"seo":89,"sku":61,"stem":90,"subjects":91,"summary":93,"tags":94,"updated":70,"__hash__":98},"courses\u002Flearn\u002Fbn\u002Fcourses\u002Ffreelancing-shuru\u002Findex.md","ফ্রিল্যান্সিং শুরু",{"type":36,"value":76,"toc":80},[77],[39,78,79],{},"একটাই লেখা এখন — পোর্টফোলিও নিয়ে। প্রস্তাব লেখা আর দাম নির্ধারণ নিয়ে\nলেখা দুটো আসছে।",{"title":46,"searchDepth":47,"depth":47,"links":81},[],"প্রথম ক্লায়েন্ট পর্যন্ত পৌঁছানোর বাস্তব ধাপগুলো।",15,"lucide:briefcase",{},"\u002Flearn\u002Fbn\u002Fcourses\u002Ffreelancing-shuru",[],[],{"title":74,"description":82},"learn\u002Fbn\u002Fcourses\u002Ffreelancing-shuru\u002Findex",[92],"career","ফ্রিল্যান্সিং শুরু করার সবচেয়ে বড় বাধা টেকনিক্যাল দক্ষতা নয় — বিশ্বাসযোগ্যতা।\nএকজন অচেনা ক্লায়েন্ট কেন আপনাকে টাকা দেবে, সেই প্রশ্নের উত্তরই আসল কাজ।\nএখানে তিনটা জিনিস নিয়ে কথা: কী দেখানোর মতো বানাবেন, প্রথম প্রস্তাব কীভাবে\nলিখবেন, আর দাম নিয়ে কথা বলার সময় কোন ভুলগুলো নতুনরা করে।\nকোনোটাই শর্টকাট নয়, কিন্তু প্রতিটাই এড়ানো যায় এমন ভুল কমায়।\n",[95,96,97],"ফ্রিল্যান্সিং","ক্যারিয়ার","ক্লায়েন্ট","6p7NKLBZjpbUNJfizYf1A-wlJcPXpxYgdDY9SrBKWAQ",{"id":100,"title":101,"access":102,"body":103,"description":107,"draft":6,"estimatedMinutes":108,"extension":51,"featured":6,"icon":109,"lang":110,"level":111,"meta":112,"navigation":24,"order":56,"path":113,"prerequisites":114,"resources":115,"seo":121,"sku":119,"stem":122,"subjects":123,"summary":125,"tags":126,"updated":70,"__hash__":131},"courses\u002Flearn\u002Fen\u002Fcourses\u002Fcaching\u002Findex.md","Caching strategies","premium",{"type":36,"value":104,"toc":105},[],{"title":46,"searchDepth":47,"depth":47,"links":106},[],"Read-through, write-behind, invalidation and the failure modes each one buys.",40,"lucide:layers","en","advanced",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fcaching",[],[116],{"label":117,"file":118,"access":102,"sku":119,"size":120},"Caching decision matrix","caching-decision-matrix.pdf","topic-caching-strategies","310 KB",{"title":101,"description":107},"learn\u002Fen\u002Fcourses\u002Fcaching\u002Findex",[124],"system-design","A cache is a bet that reading stale data is cheaper than reading correct data.\nThe strategies differ in who writes to the cache, when, and what happens when\nthe cache and the source disagree. Cache-aside is the default because it fails\nsafely. Write-behind is the fastest and loses data on a crash. Almost every\nproduction incident involving a cache comes down to invalidation, stampedes, or\na cache that was quietly doing nothing at all.\n",[127,128,129,130],"caching","performance","consistency","redis","mqYWifp5mzccLyzSY681N-Yn1wymognvHpRRfhOtFBI",{"id":133,"title":134,"access":34,"body":135,"description":139,"draft":6,"estimatedMinutes":140,"extension":51,"featured":24,"icon":141,"lang":110,"level":142,"meta":143,"navigation":24,"order":56,"path":144,"prerequisites":145,"resources":146,"seo":151,"sku":61,"stem":152,"subjects":153,"summary":154,"tags":155,"updated":160,"__hash__":161},"courses\u002Flearn\u002Fen\u002Fcourses\u002Fevent-loop\u002Findex.md","The event loop",{"type":36,"value":136,"toc":137},[],{"title":46,"searchDepth":47,"depth":47,"links":138},[],"Call stack, task queues and microtasks — why your code ran in that order.",30,"lucide:workflow","intermediate",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fevent-loop",[],[147],{"label":148,"file":149,"access":34,"size":150},"Event loop reference card","js-event-loop-card.pdf","95 KB",{"title":134,"description":139},"learn\u002Fen\u002Fcourses\u002Fevent-loop\u002Findex",[64],"JavaScript runs on one thread with one call stack. Anything asynchronous is\nhanded to the host environment, which puts a callback in a queue when it is\nready. The event loop moves work from those queues onto the stack, but only\nwhen the stack is empty. Microtasks — promise continuations — drain completely\nbetween each task, which is why an await resolves before a setTimeout of zero.\nAlmost every ordering surprise in JavaScript follows from those rules.\n",[156,157,158,159],"async","promises","runtime","concurrency","2026-08-05","BOC7v_GWQoKJp-mEK6X_H0pY-0KcxkhIWnXKWsm8c9k",{"id":163,"title":164,"access":34,"body":165,"description":169,"draft":6,"estimatedMinutes":170,"extension":51,"featured":6,"icon":171,"lang":110,"level":54,"meta":172,"navigation":24,"order":56,"path":173,"prerequisites":174,"resources":175,"seo":180,"sku":61,"stem":181,"subjects":182,"summary":184,"tags":185,"updated":190,"__hash__":191},"courses\u002Flearn\u002Fen\u002Fcourses\u002Fpods\u002Findex.md","Pods and workloads",{"type":36,"value":166,"toc":167},[],{"title":46,"searchDepth":47,"depth":47,"links":168},[],"The smallest deployable unit, and the controllers that manage sets of them.",25,"lucide:blocks",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fpods",[],[176],{"label":177,"file":178,"access":34,"size":179},"Workload controller decision tree","k8s-workload-controllers.pdf","120 KB",{"title":164,"description":169},"learn\u002Fen\u002Fcourses\u002Fpods\u002Findex",[183],"kubernetes","A pod is one or more containers sharing a network namespace and a lifecycle.\nYou rarely create one directly — Deployments, StatefulSets, DaemonSets and Jobs\neach manage pods with different guarantees about identity, ordering and\nreplacement. Choosing the wrong controller is the root of a surprising number\nof production problems, because the guarantee you assumed was never offered.\n",[186,187,188,189],"pods","deployments","statefulsets","workloads","2026-08-04","m76OXiUC-jLIAp9S_k3i7kuptZfYW5jrdGa40vEITF8",{"id":193,"title":194,"access":34,"body":195,"description":199,"draft":6,"estimatedMinutes":200,"extension":51,"featured":24,"icon":201,"lang":110,"level":142,"meta":202,"navigation":24,"order":203,"path":204,"prerequisites":205,"resources":207,"seo":217,"sku":61,"stem":218,"subjects":219,"summary":220,"tags":221,"updated":70,"__hash__":226},"courses\u002Flearn\u002Fen\u002Fcourses\u002Fnetworking\u002Findex.md","Kubernetes Networking",{"type":36,"value":196,"toc":197},[],{"title":46,"searchDepth":47,"depth":47,"links":198},[],"Services, Ingress and network policy — how a packet actually reaches a pod.",45,"lucide:network",{},2,"\u002Flearn\u002Fen\u002Fcourses\u002Fnetworking",[206],"\u002Flearn\u002Fen\u002Fkubernetes\u002Fpods",[208,212],{"label":209,"file":210,"access":34,"size":211},"Service types cheat sheet","k8s-service-types.pdf","180 KB",{"label":213,"file":214,"access":102,"sku":215,"size":216},"NetworkPolicy recipe pack","k8s-network-policies.pdf","res-k8s-netpol","640 KB",{"title":194,"description":199},"learn\u002Fen\u002Fcourses\u002Fnetworking\u002Findex",[183],"Kubernetes networking rests on one rule: every pod gets its own IP and can\nreach every other pod without NAT. Everything above that — Services, Ingress,\nNetworkPolicy — exists because pod IPs are not stable and not exposed.\nServices give you a stable virtual IP and load balancing. Ingress puts an\nHTTP router in front of Services so many hostnames share one entry point.\nNetworkPolicy takes the default-allow flat network and narrows it down. Get\nthose three roles clear and the failure modes stop being mysterious.\n",[222,223,224,225],"networking","services","ingress","cni","GumIppvDWYP1scgT_rOvScFGIQ-Wqs-Uax-NJtxnUbU",[228,490,571,779,1134,1461,1746,2070,2308,2717,3137,3421],{"id":229,"title":230,"access":34,"body":231,"description":484,"extension":51,"lang":53,"meta":485,"navigation":24,"order":56,"partial":6,"path":486,"seo":487,"stem":488,"__hash__":489},"lessons\u002Flearn\u002Fbn\u002Fcourses\u002Fevent-loop\u002F01.call-stack.md","কল স্ট্যাক",{"type":36,"value":232,"toc":480},[233,237,240,345,366,371,374,377,440,443,449,453,464,467,476],[234,235,230],"h1",{"id":236},"কল-স্ট্যাক",[39,238,239],{},"জাভাস্ক্রিপ্ট যখন একটা ফাংশন ডাকে, সেটা একটা স্ট্যাকের উপরে বসে। ফাংশনটা শেষ\nহলে স্ট্যাক থেকে নেমে যায়। শেষেরটা আগে নামে — এটাই স্ট্যাক।",[241,242,246],"pre",{"className":243,"code":244,"language":245,"meta":46,"style":46},"language-js shiki shiki-themes github-dark-dimmed github-dark-dimmed","function ভেতরে() {\n  console.log('ভেতরের কাজ')\n}\n\nfunction বাইরে() {\n  ভেতরে()\n  console.log('বাইরের কাজ')\n}\n\nবাইরে()\n","js",[247,248,249,265,283,288,294,304,313,327,332,337],"code",{"__ignoreMap":46},[250,251,253,257,261],"span",{"class":252,"line":56},"line",[250,254,256],{"class":255},"sEYkB","function",[250,258,260],{"class":259},"s-wk_"," ভেতরে",[250,262,264],{"class":263},"sM9_K","() {\n",[250,266,267,270,273,276,280],{"class":252,"line":203},[250,268,269],{"class":263},"  console.",[250,271,272],{"class":259},"log",[250,274,275],{"class":263},"(",[250,277,279],{"class":278},"szYpP","'ভেতরের কাজ'",[250,281,282],{"class":263},")\n",[250,284,285],{"class":252,"line":47},[250,286,287],{"class":263},"}\n",[250,289,291],{"class":252,"line":290},4,[250,292,293],{"emptyLinePlaceholder":24},"\n",[250,295,297,299,302],{"class":252,"line":296},5,[250,298,256],{"class":255},[250,300,301],{"class":259}," বাইরে",[250,303,264],{"class":263},[250,305,307,310],{"class":252,"line":306},6,[250,308,309],{"class":259},"  ভেতরে",[250,311,312],{"class":263},"()\n",[250,314,316,318,320,322,325],{"class":252,"line":315},7,[250,317,269],{"class":263},[250,319,272],{"class":259},[250,321,275],{"class":263},[250,323,324],{"class":278},"'বাইরের কাজ'",[250,326,282],{"class":263},[250,328,330],{"class":252,"line":329},8,[250,331,287],{"class":263},[250,333,335],{"class":252,"line":334},9,[250,336,293],{"emptyLinePlaceholder":24},[250,338,340,343],{"class":252,"line":339},10,[250,341,342],{"class":259},"বাইরে",[250,344,312],{"class":263},[39,346,347,350,351,354,355,358,359,362,363,365],{},[247,348,349],{},"বাইরে()"," ডাকার পর স্ট্যাক দাঁড়ায় ",[247,352,353],{},"[বাইরে]",", তারপর ",[247,356,357],{},"[বাইরে, ভেতরে]","। ",[247,360,361],{},"ভেতরে","\nশেষ হলে নেমে যায়, ",[247,364,342],{}," তার পরের লাইনে যায়, তারপর সেও নামে।",[367,368,370],"h2",{"id":369},"স্ট্যাক-একটাই","স্ট্যাক একটাই",[39,372,373],{},"এটাই আসল কথা। ব্রাউজারে জাভাস্ক্রিপ্টের একটাই কল স্ট্যাক, মানে একটাই থ্রেড।\nএক সময়ে একটাই কাজ চলতে পারে।",[39,375,376],{},"তাই যদি একটা ফাংশন অনেকক্ষণ ধরে চলে, আর কিছুই চলতে পারে না — ক্লিক কাজ করে না,\nঅ্যানিমেশন থেমে যায়, স্ক্রল আটকে যায়:",[241,378,380],{"className":243,"code":379,"language":245,"meta":46,"style":46},"const শুরু = Date.now()\nwhile (Date.now() - শুরু \u003C 5000) {\n  \u002F\u002F পাঁচ সেকেন্ড ধরে স্ট্যাক দখল করে রাখা\n}\n",[247,381,382,402,430,436],{"__ignoreMap":46},[250,383,384,387,391,394,397,400],{"class":252,"line":56},[250,385,386],{"class":255},"const",[250,388,390],{"class":389},"sQdni"," শুরু",[250,392,393],{"class":255}," =",[250,395,396],{"class":263}," Date.",[250,398,399],{"class":259},"now",[250,401,312],{"class":263},[250,403,404,407,410,412,415,418,421,424,427],{"class":252,"line":203},[250,405,406],{"class":255},"while",[250,408,409],{"class":263}," (Date.",[250,411,399],{"class":259},[250,413,414],{"class":263},"() ",[250,416,417],{"class":255},"-",[250,419,420],{"class":263}," শুরু ",[250,422,423],{"class":255},"\u003C",[250,425,426],{"class":389}," 5000",[250,428,429],{"class":263},") {\n",[250,431,432],{"class":252,"line":47},[250,433,435],{"class":434},"s0RzX","  \u002F\u002F পাঁচ সেকেন্ড ধরে স্ট্যাক দখল করে রাখা\n",[250,437,438],{"class":252,"line":290},[250,439,287],{"class":263},[39,441,442],{},"এই পাঁচ সেকেন্ড পেজটা পুরোপুরি জমে থাকবে। \"পেজ হ্যাং করেছে\" বলতে সাধারণত\nএটাই বোঝায় — কোনো একটা কাজ স্ট্যাক ছাড়ছে না।",[444,445,446],"note",{},[39,447,448],{},"ব্রাউজার রেন্ডারিংও একই থ্রেডে হয়। তাই স্ট্যাক ব্যস্ত মানে শুধু আপনার কোড নয়,\nপর্দায় আঁকার কাজটাও থেমে থাকে।",[367,450,452],{"id":451},"তাহলে-অ্যাসিঙ্ক-কাজ-হয়-কীভাবে","তাহলে অ্যাসিঙ্ক কাজ হয় কীভাবে",[39,454,455,456,459,460,463],{},"এখানেই ভুল ধারণাটা তৈরি হয়। ",[247,457,458],{},"fetch()"," বা ",[247,461,462],{},"setTimeout()"," জাভাস্ক্রিপ্ট নিজে\nকরে না। এগুলো ব্রাউজারের দেওয়া সুবিধা, আর ব্রাউজারের নিজের আলাদা থ্রেড আছে।",[39,465,466],{},"জাভাস্ক্রিপ্ট শুধু বলে দেয় \"এই কাজটা করো, শেষ হলে এই ফাংশনটা ডেকো\" — তারপর\nসাথে সাথে পরের লাইনে চলে যায়। স্ট্যাক খালি থাকে।",[39,468,469,470,475],{},"কাজটা শেষ হলে ব্রাউজার সেই ফাংশনটা কোথায় রাখে, আর কখন সেটা চলার সুযোগ পায় —\nসেটাই ",[471,472,474],"a",{"href":473},"\u002Flearn\u002Fbn\u002Fjavascript\u002Fevent-loop\u002Fdocs\u002Ftask-queues","পরের লেখার"," বিষয়।",[477,478,479],"style",{},"html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}",{"title":46,"searchDepth":47,"depth":47,"links":481},[482,483],{"id":369,"depth":203,"text":370},{"id":451,"depth":203,"text":452},"এক থ্রেড, এক স্ট্যাক — আর এর ফলে কী কী হয়।",{},"\u002Flearn\u002Fbn\u002Fcourses\u002Fevent-loop\u002Fcall-stack",{"title":230,"description":484},"learn\u002Fbn\u002Fcourses\u002Fevent-loop\u002F01.call-stack","bBbpl-84LEbDnPJWxkorGp1Y31DKL64tqsCNK09L5O4",{"id":491,"title":492,"access":34,"body":493,"description":565,"extension":51,"lang":53,"meta":566,"navigation":24,"order":56,"partial":6,"path":567,"seo":568,"stem":569,"__hash__":570},"lessons\u002Flearn\u002Fbn\u002Fcourses\u002Ffreelancing-shuru\u002F01.portfolio.md","পোর্টফোলিও",{"type":36,"value":494,"toc":561},[495,497,500,503,507,510,539,545,549,552,555],[234,496,492],{"id":492},[39,498,499],{},"নতুনদের পোর্টফোলিওর সবচেয়ে সাধারণ সমস্যা হলো সেটা টিউটোরিয়ালের তালিকা।\nটু-ডু অ্যাপ, ওয়েদার অ্যাপ, ক্যালকুলেটর।",[39,501,502],{},"সমস্যাটা মান নিয়ে নয়। সমস্যাটা হলো এগুলো কোনো প্রশ্নের উত্তর দেয় না।\nক্লায়েন্ট জানতে চায় \"আমার সমস্যাটা এই লোক সমাধান করতে পারবে কি না\" — আর\nটু-ডু অ্যাপ সেই প্রশ্নের কোনো উত্তর দেয় না, কারণ ওটার সমস্যাটা আগেই কেউ\nসংজ্ঞায়িত করে দিয়েছে।",[367,504,506],{"id":505},"তিনটার-বেশি-লাগে-না","তিনটার বেশি লাগে না",[39,508,509],{},"একটা ভালো কেস স্টাডি দশটা স্ক্রিনশটের চেয়ে বেশি কাজ করে। প্রতিটার জন্য\nচারটা জিনিস লিখুন:",[511,512,513,521,527,533],"ol",{},[514,515,516,520],"li",{},[517,518,519],"strong",{},"সমস্যা কী ছিল"," — এক অনুচ্ছেদ, টেকনিক্যাল ভাষা ছাড়া।",[514,522,523,526],{},[517,524,525],{},"কী করেছেন"," — সিদ্ধান্তগুলো, শুধু টুলের নাম নয়।",[514,528,529,532],{},[517,530,531],{},"কী বাদ দিয়েছেন এবং কেন"," — এটাই সবচেয়ে বেশি ছাপ ফেলে।",[514,534,535,538],{},[517,536,537],{},"ফলাফল"," — সংখ্যা থাকলে সংখ্যা, না থাকলে সৎ বর্ণনা।",[540,541,542],"tip",{},[39,543,544],{},"তৃতীয় পয়েন্টটা বেশিরভাগ মানুষ বাদ দেয়। অথচ \"কী বানাইনি এবং কেন\" পড়লে\nবোঝা যায় আপনি ট্রেড-অফ বোঝেন — যেটা কোড লিখতে পারার চেয়ে বিরল গুণ।",[367,546,548],{"id":547},"আসল-কাজ-না-থাকলে","আসল কাজ না থাকলে",[39,550,551],{},"নিজের সমস্যা সমাধান করুন। যে জিনিসটা আপনাকে বিরক্ত করে, সেটার জন্য কিছু\nবানান। এটা টিউটোরিয়াল প্রজেক্টের চেয়ে ভালো, কারণ সমস্যাটা আপনি নিজে\nসংজ্ঞায়িত করেছেন — আর সেটাই দেখানোর জিনিস।",[39,553,554],{},"ওপেন সোর্সে ছোট অবদানও কাজে দেয়। একটা মার্জ হওয়া পুল রিকোয়েস্ট প্রমাণ\nকরে যে আপনি অন্যের কোডবেসে ঢুকে কাজ করতে পারেন — যা প্রায় প্রতিটা ক্লায়েন্ট\nপ্রজেক্টেই লাগে।",[556,557,558],"warning",{},[39,559,560],{},"স্ক্রিনশট ছাড়া লিংক দেবেন না, আর ভাঙা লিংক দেবেন না। যে সাইটটা খোলে না,\nসেটা না থাকার চেয়ে খারাপ — কারণ ওটা যত্নের অভাব দেখায়।",{"title":46,"searchDepth":47,"depth":47,"links":562},[563,564],{"id":505,"depth":203,"text":506},{"id":547,"depth":203,"text":548},"কী দেখাবেন, আর কী দেখানোর দরকার নেই।",{},"\u002Flearn\u002Fbn\u002Fcourses\u002Ffreelancing-shuru\u002Fportfolio",{"title":492,"description":565},"learn\u002Fbn\u002Fcourses\u002Ffreelancing-shuru\u002F01.portfolio","Bnq7tbmbDDHu7NHTxpi-0fJ472ltP4NfcjzHpk2MhTE",{"id":572,"title":573,"access":34,"body":574,"description":773,"extension":51,"lang":110,"meta":774,"navigation":24,"order":56,"partial":24,"path":775,"seo":776,"stem":777,"__hash__":778},"lessons\u002Flearn\u002Fen\u002Fcourses\u002Fcaching\u002F01.strategies.md","The four strategies",{"type":36,"value":575,"toc":767},[576,579,582,586,589,726,729,733,736,739,743,746,749,753,756,759,764],[234,577,573],{"id":578},"the-four-strategies",[39,580,581],{},"Every caching pattern answers two questions: who populates the cache, and when is\nthe source of truth updated relative to it.",[367,583,585],{"id":584},"cache-aside","Cache-aside",[39,587,588],{},"The application checks the cache, and on a miss reads the database and populates\nthe cache itself. The cache has no knowledge of the database.",[241,590,594],{"className":591,"code":592,"language":593,"meta":46,"style":46},"language-python shiki shiki-themes github-dark-dimmed github-dark-dimmed","def get_user(user_id):\n    cached = cache.get(f\"user:{user_id}\")\n    if cached is not None:\n        return cached\n    user = db.query(\"SELECT * FROM users WHERE id = %s\", user_id)\n    cache.set(f\"user:{user_id}\", user, ttl=300)\n    return user\n","python",[247,595,596,607,638,658,666,687,718],{"__ignoreMap":46},[250,597,598,601,604],{"class":252,"line":56},[250,599,600],{"class":255},"def",[250,602,603],{"class":259}," get_user",[250,605,606],{"class":263},"(user_id):\n",[250,608,609,612,615,618,621,624,627,630,633,636],{"class":252,"line":203},[250,610,611],{"class":263},"    cached ",[250,613,614],{"class":255},"=",[250,616,617],{"class":263}," cache.get(",[250,619,620],{"class":255},"f",[250,622,623],{"class":278},"\"user:",[250,625,626],{"class":255},"{",[250,628,629],{"class":263},"user_id",[250,631,632],{"class":255},"}",[250,634,635],{"class":278},"\"",[250,637,282],{"class":263},[250,639,640,643,646,649,652,655],{"class":252,"line":47},[250,641,642],{"class":255},"    if",[250,644,645],{"class":263}," cached ",[250,647,648],{"class":255},"is",[250,650,651],{"class":255}," not",[250,653,654],{"class":389}," None",[250,656,657],{"class":263},":\n",[250,659,660,663],{"class":252,"line":290},[250,661,662],{"class":255},"        return",[250,664,665],{"class":263}," cached\n",[250,667,668,671,673,676,679,682,684],{"class":252,"line":296},[250,669,670],{"class":263},"    user ",[250,672,614],{"class":255},[250,674,675],{"class":263}," db.query(",[250,677,678],{"class":278},"\"SELECT * FROM users WHERE id = ",[250,680,681],{"class":255},"%s",[250,683,635],{"class":278},[250,685,686],{"class":263},", user_id)\n",[250,688,689,692,694,696,698,700,702,704,707,711,713,716],{"class":252,"line":306},[250,690,691],{"class":263},"    cache.set(",[250,693,620],{"class":255},[250,695,623],{"class":278},[250,697,626],{"class":255},[250,699,629],{"class":263},[250,701,632],{"class":255},[250,703,635],{"class":278},[250,705,706],{"class":263},", user, ",[250,708,710],{"class":709},"siJwO","ttl",[250,712,614],{"class":255},[250,714,715],{"class":389},"300",[250,717,282],{"class":263},[250,719,720,723],{"class":252,"line":315},[250,721,722],{"class":255},"    return",[250,724,725],{"class":263}," user\n",[39,727,728],{},"This is the default for a reason: if the cache is down, every request simply\nbecomes a database read. Degraded, not broken.",[367,730,732],{"id":731},"read-through","Read-through",[39,734,735],{},"The cache sits inline. The application only talks to the cache, which fetches from\nthe database on a miss. Logic moves out of the application and into the cache layer.",[39,737,738],{},"Cleaner call sites, but the cache is now on the critical path — if it is unavailable,\nreads fail entirely rather than falling back.",[367,740,742],{"id":741},"write-through","Write-through",[39,744,745],{},"Writes go to the cache, which synchronously writes to the database before\nacknowledging. The cache is never stale.",[39,747,748],{},"The cost is write latency: every write pays for both hops. Worth it when reads\nvastly outnumber writes and stale reads are unacceptable.",[367,750,752],{"id":751},"write-behind","Write-behind",[39,754,755],{},"Writes land in the cache and are flushed to the database asynchronously.",[39,757,758],{},"Fastest writes available, and the only strategy that can lose acknowledged data. A\ncrash between acknowledgement and flush loses whatever was buffered.",[556,760,761],{},[39,762,763],{},"Write-behind is appropriate for metrics, view counts and analytics events. It is not\nappropriate for anything a user would notice going missing — orders, payments,\nmessages. The performance gain is real, and so is the data loss.",[477,765,766],{},"html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .siJwO, html code.shiki .siJwO{--shiki-default:#F69D50;--shiki-dark:#F69D50}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":47,"depth":47,"links":768},[769,770,771,772],{"id":584,"depth":203,"text":585},{"id":731,"depth":203,"text":732},{"id":741,"depth":203,"text":742},{"id":751,"depth":203,"text":752},"Cache-aside, read-through, write-through and write-behind compared.",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fcaching\u002Fstrategies",{"title":573,"description":773},"learn\u002Fen\u002Fcourses\u002Fcaching\u002F01.strategies","sh2NUIv6r7JjhJWD_OzLrfaRSj7urg2L7nJtqWFgm04",{"id":780,"title":781,"access":34,"body":782,"description":1128,"extension":51,"lang":110,"meta":1129,"navigation":24,"order":56,"partial":6,"path":1130,"seo":1131,"stem":1132,"__hash__":1133},"lessons\u002Flearn\u002Fen\u002Fcourses\u002Fevent-loop\u002F01.call-stack.md","The call stack",{"type":36,"value":783,"toc":1123},[784,787,790,870,877,881,884,887,934,939,943,951,962,1027,1032,1036,1039,1108,1121],[234,785,781],{"id":786},"the-call-stack",[39,788,789],{},"JavaScript has a single call stack. One thing executes at a time, and a function\nruns to completion before anything else gets a turn.",[241,791,793],{"className":243,"code":792,"language":245,"meta":46,"style":46},"function third() {\n  return 'done'\n}\nfunction second() {\n  return third()\n}\nfunction first() {\n  return second()\n}\n\nfirst()\n",[247,794,795,804,812,816,825,833,837,846,854,858,862],{"__ignoreMap":46},[250,796,797,799,802],{"class":252,"line":56},[250,798,256],{"class":255},[250,800,801],{"class":259}," third",[250,803,264],{"class":263},[250,805,806,809],{"class":252,"line":203},[250,807,808],{"class":255},"  return",[250,810,811],{"class":278}," 'done'\n",[250,813,814],{"class":252,"line":47},[250,815,287],{"class":263},[250,817,818,820,823],{"class":252,"line":290},[250,819,256],{"class":255},[250,821,822],{"class":259}," second",[250,824,264],{"class":263},[250,826,827,829,831],{"class":252,"line":296},[250,828,808],{"class":255},[250,830,801],{"class":259},[250,832,312],{"class":263},[250,834,835],{"class":252,"line":306},[250,836,287],{"class":263},[250,838,839,841,844],{"class":252,"line":315},[250,840,256],{"class":255},[250,842,843],{"class":259}," first",[250,845,264],{"class":263},[250,847,848,850,852],{"class":252,"line":329},[250,849,808],{"class":255},[250,851,822],{"class":259},[250,853,312],{"class":263},[250,855,856],{"class":252,"line":334},[250,857,287],{"class":263},[250,859,860],{"class":252,"line":339},[250,861,293],{"emptyLinePlaceholder":24},[250,863,865,868],{"class":252,"line":864},11,[250,866,867],{"class":259},"first",[250,869,312],{"class":263},[39,871,872,873,876],{},"The stack grows to ",[247,874,875],{},"first → second → third",", then unwinds. Nothing interleaves.",[367,878,880],{"id":879},"run-to-completion-is-a-guarantee","Run-to-completion is a guarantee",[39,882,883],{},"Once a function starts, no other JavaScript can run until it returns. This is why\nyou never need locks around a shared variable: no other code can observe an\nintermediate state.",[39,885,886],{},"It is also why one slow function freezes everything:",[241,888,890],{"className":243,"code":889,"language":245,"meta":46,"style":46},"const start = Date.now()\nwhile (Date.now() - start \u003C 5000) {}\n\u002F\u002F Nothing else runs for five seconds — no clicks, no timers, no rendering.\n",[247,891,892,907,929],{"__ignoreMap":46},[250,893,894,896,899,901,903,905],{"class":252,"line":56},[250,895,386],{"class":255},[250,897,898],{"class":389}," start",[250,900,393],{"class":255},[250,902,396],{"class":263},[250,904,399],{"class":259},[250,906,312],{"class":263},[250,908,909,911,913,915,917,919,922,924,926],{"class":252,"line":203},[250,910,406],{"class":255},[250,912,409],{"class":263},[250,914,399],{"class":259},[250,916,414],{"class":263},[250,918,417],{"class":255},[250,920,921],{"class":263}," start ",[250,923,423],{"class":255},[250,925,426],{"class":389},[250,927,928],{"class":263},") {}\n",[250,930,931],{"class":252,"line":47},[250,932,933],{"class":434},"\u002F\u002F Nothing else runs for five seconds — no clicks, no timers, no rendering.\n",[556,935,936],{},[39,937,938],{},"\"Blocking the event loop\" is not a metaphor. On the main thread of a browser, a\nlong synchronous loop blocks rendering, input handling and every pending callback.\nThe tab is frozen for the duration, and the browser may offer to kill it.",[367,940,942],{"id":941},"where-async-work-actually-happens","Where async work actually happens",[39,944,945,946,950],{},"The runtime is single-threaded. The ",[947,948,949],"em",{},"environment"," around it is not.",[39,952,953,954,957,958,961],{},"When you call ",[247,955,956],{},"setTimeout",", the timer is not JavaScript — it is the host. When you\ncall ",[247,959,960],{},"fetch",", the network request is handled by the browser's networking stack on\nanother thread. Your callback is simply registered.",[241,963,965],{"className":243,"code":964,"language":245,"meta":46,"style":46},"console.log('1')\nsetTimeout(() => console.log('3'), 0)\nconsole.log('2')\n\u002F\u002F 1, 2, 3\n",[247,966,967,981,1009,1022],{"__ignoreMap":46},[250,968,969,972,974,976,979],{"class":252,"line":56},[250,970,971],{"class":263},"console.",[250,973,272],{"class":259},[250,975,275],{"class":263},[250,977,978],{"class":278},"'1'",[250,980,282],{"class":263},[250,982,983,985,988,991,994,996,998,1001,1004,1007],{"class":252,"line":203},[250,984,956],{"class":259},[250,986,987],{"class":263},"(() ",[250,989,990],{"class":255},"=>",[250,992,993],{"class":263}," console.",[250,995,272],{"class":259},[250,997,275],{"class":263},[250,999,1000],{"class":278},"'3'",[250,1002,1003],{"class":263},"), ",[250,1005,1006],{"class":389},"0",[250,1008,282],{"class":263},[250,1010,1011,1013,1015,1017,1020],{"class":252,"line":47},[250,1012,971],{"class":263},[250,1014,272],{"class":259},[250,1016,275],{"class":263},[250,1018,1019],{"class":278},"'2'",[250,1021,282],{"class":263},[250,1023,1024],{"class":252,"line":290},[250,1025,1026],{"class":434},"\u002F\u002F 1, 2, 3\n",[39,1028,1029,1031],{},[247,1030,956],{}," returns immediately. The callback is queued for later even with a zero\ndelay, because \"later\" means \"once the stack is empty\" — not \"in zero milliseconds\".",[367,1033,1035],{"id":1034},"the-stack-in-a-stack-trace","The stack in a stack trace",[39,1037,1038],{},"An error captures the stack at the moment it is thrown. In async code, the stack\nyou get is the one at callback time, not at scheduling time:",[241,1040,1042],{"className":243,"code":1041,"language":245,"meta":46,"style":46},"function schedule() {\n  setTimeout(() => {\n    throw new Error('boom')\n  }, 0)\n}\nschedule()\n\u002F\u002F The trace shows the timer callback. `schedule` is long gone.\n",[247,1043,1044,1053,1065,1083,1092,1096,1103],{"__ignoreMap":46},[250,1045,1046,1048,1051],{"class":252,"line":56},[250,1047,256],{"class":255},[250,1049,1050],{"class":259}," schedule",[250,1052,264],{"class":263},[250,1054,1055,1058,1060,1062],{"class":252,"line":203},[250,1056,1057],{"class":259},"  setTimeout",[250,1059,987],{"class":263},[250,1061,990],{"class":255},[250,1063,1064],{"class":263}," {\n",[250,1066,1067,1070,1073,1076,1078,1081],{"class":252,"line":47},[250,1068,1069],{"class":255},"    throw",[250,1071,1072],{"class":255}," new",[250,1074,1075],{"class":259}," Error",[250,1077,275],{"class":263},[250,1079,1080],{"class":278},"'boom'",[250,1082,282],{"class":263},[250,1084,1085,1088,1090],{"class":252,"line":290},[250,1086,1087],{"class":263},"  }, ",[250,1089,1006],{"class":389},[250,1091,282],{"class":263},[250,1093,1094],{"class":252,"line":296},[250,1095,287],{"class":263},[250,1097,1098,1101],{"class":252,"line":306},[250,1099,1100],{"class":259},"schedule",[250,1102,312],{"class":263},[250,1104,1105],{"class":252,"line":315},[250,1106,1107],{"class":434},"\u002F\u002F The trace shows the timer callback. `schedule` is long gone.\n",[39,1109,1110,1111,1114,1115,1117,1118,1120],{},"Modern engines reconstruct async stack traces across ",[247,1112,1113],{},"await"," boundaries, which is\none practical reason to prefer ",[247,1116,156],{},"\u002F",[247,1119,1113],{}," over raw callbacks.",[477,1122,479],{},{"title":46,"searchDepth":47,"depth":47,"links":1124},[1125,1126,1127],{"id":879,"depth":203,"text":880},{"id":941,"depth":203,"text":942},{"id":1034,"depth":203,"text":1035},"One thread, one stack, and what \"blocking\" actually blocks.",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fevent-loop\u002Fcall-stack",{"title":781,"description":1128},"learn\u002Fen\u002Fcourses\u002Fevent-loop\u002F01.call-stack","BiJN8dMZjgvIkocYL4WlAg539TRcbZZHnuwy9Hi7GFg",{"id":1135,"title":1136,"access":34,"body":1137,"description":1455,"extension":51,"lang":110,"meta":1456,"navigation":24,"order":56,"partial":6,"path":1457,"seo":1458,"stem":1459,"__hash__":1460},"lessons\u002Flearn\u002Fen\u002Fcourses\u002Fnetworking\u002F01.services.md","Services",{"type":36,"value":1138,"toc":1448},[1139,1141,1144,1151,1155,1162,1261,1268,1273,1277,1346,1352,1356,1363,1366,1379,1383,1392,1395,1399,1406,1442,1445],[234,1140,1136],{"id":223},[39,1142,1143],{},"A pod IP is an unstable identifier. Pods are rescheduled, replaced on deploy, and\nscaled in and out. Anything that hard-codes a pod IP breaks on the first restart.",[39,1145,1146,1147,1150],{},"A ",[517,1148,1149],{},"Service"," is a stable name and virtual IP in front of a changing set of pods.",[367,1152,1154],{"id":1153},"how-the-selector-works","How the selector works",[39,1156,1157,1158,1161],{},"A Service does not reference pods directly. It declares a label selector, and a\ncontroller keeps a matching ",[247,1159,1160],{},"EndpointSlice"," up to date:",[241,1163,1167],{"className":1164,"code":1165,"language":1166,"meta":46,"style":46},"language-yaml shiki shiki-themes github-dark-dimmed github-dark-dimmed","apiVersion: v1\nkind: Service\nmetadata:\n  name: payments\nspec:\n  selector:\n    app: payments\n  ports:\n    - port: 80\n      targetPort: 8080\n","yaml",[247,1168,1169,1181,1191,1198,1208,1215,1222,1231,1238,1251],{"__ignoreMap":46},[250,1170,1171,1175,1178],{"class":252,"line":56},[250,1172,1174],{"class":1173},"sza-u","apiVersion",[250,1176,1177],{"class":263},": ",[250,1179,1180],{"class":278},"v1\n",[250,1182,1183,1186,1188],{"class":252,"line":203},[250,1184,1185],{"class":1173},"kind",[250,1187,1177],{"class":263},[250,1189,1190],{"class":278},"Service\n",[250,1192,1193,1196],{"class":252,"line":47},[250,1194,1195],{"class":1173},"metadata",[250,1197,657],{"class":263},[250,1199,1200,1203,1205],{"class":252,"line":290},[250,1201,1202],{"class":1173},"  name",[250,1204,1177],{"class":263},[250,1206,1207],{"class":278},"payments\n",[250,1209,1210,1213],{"class":252,"line":296},[250,1211,1212],{"class":1173},"spec",[250,1214,657],{"class":263},[250,1216,1217,1220],{"class":252,"line":306},[250,1218,1219],{"class":1173},"  selector",[250,1221,657],{"class":263},[250,1223,1224,1227,1229],{"class":252,"line":315},[250,1225,1226],{"class":1173},"    app",[250,1228,1177],{"class":263},[250,1230,1207],{"class":278},[250,1232,1233,1236],{"class":252,"line":329},[250,1234,1235],{"class":1173},"  ports",[250,1237,657],{"class":263},[250,1239,1240,1243,1246,1248],{"class":252,"line":334},[250,1241,1242],{"class":263},"    - ",[250,1244,1245],{"class":1173},"port",[250,1247,1177],{"class":263},[250,1249,1250],{"class":389},"80\n",[250,1252,1253,1256,1258],{"class":252,"line":339},[250,1254,1255],{"class":1173},"      targetPort",[250,1257,1177],{"class":263},[250,1259,1260],{"class":389},"8080\n",[39,1262,1263,1264,1267],{},"Any pod carrying ",[247,1265,1266],{},"app: payments"," and passing its readiness probe is added to the\nendpoint list. Any pod that fails readiness is removed. This is the reconciliation\nmodel again: the Service is a declaration, the endpoint list is reality, and a\ncontroller closes the gap.",[444,1269,1270],{},[39,1271,1272],{},"A Service with a selector that matches nothing is not an error. It is an empty\nendpoint list, and connections to it fail with connection refused rather than a\nclear message. This is the single most common \"my Service is broken\" cause.",[367,1274,1276],{"id":1275},"the-three-types","The three types",[1278,1279,1280,1296],"table",{},[1281,1282,1283],"thead",{},[1284,1285,1286,1290,1293],"tr",{},[1287,1288,1289],"th",{},"Type",[1287,1291,1292],{},"Allocates",[1287,1294,1295],{},"Reachable from",[1297,1298,1299,1313,1330],"tbody",{},[1284,1300,1301,1307,1310],{},[1302,1303,1304],"td",{},[247,1305,1306],{},"ClusterIP",[1302,1308,1309],{},"A virtual IP inside the cluster",[1302,1311,1312],{},"Inside the cluster only",[1284,1314,1315,1320,1327],{},[1302,1316,1317],{},[247,1318,1319],{},"NodePort",[1302,1321,1322,1323,1326],{},"A ClusterIP ",[517,1324,1325],{},"plus"," the same port on every node",[1302,1328,1329],{},"Anything that can reach a node",[1284,1331,1332,1337,1343],{},[1302,1333,1334],{},[247,1335,1336],{},"LoadBalancer",[1302,1338,1339,1340,1342],{},"A NodePort ",[517,1341,1325],{}," an external load balancer",[1302,1344,1345],{},"The internet, via the cloud provider",[39,1347,1348,1349,1351],{},"The types are cumulative, not alternatives. A ",[247,1350,1336],{}," Service still has a\nClusterIP and still has a node port — the cloud controller simply provisions an\nexternal balancer that points at those node ports.",[367,1353,1355],{"id":1354},"what-clusterip-really-is","What ClusterIP really is",[39,1357,1358,1359,1362],{},"The ClusterIP is not assigned to any interface. Nothing answers ARP for it. It\nexists only as a set of rules in each node's kernel — iptables or IPVS entries\ninstalled by ",[247,1360,1361],{},"kube-proxy"," — that rewrite the destination address to a real pod IP\nas the packet leaves.",[39,1364,1365],{},"That has a practical consequence worth internalising:",[556,1367,1368],{},[39,1369,1370,1371,1374,1375,1378],{},"You cannot ",[247,1372,1373],{},"ping"," a ClusterIP. There is no host to answer ICMP — only DNAT rules\nfor the ports the Service declares. A failed ping proves nothing about whether\nthe Service works. Test with ",[247,1376,1377],{},"curl"," against a declared port instead.",[367,1380,1382],{"id":1381},"load-balancing-granularity","Load balancing granularity",[39,1384,1385,1387,1388,1391],{},[247,1386,1361],{}," picks an endpoint per ",[517,1389,1390],{},"connection",", not per request. For HTTP\u002F1.1\nwith connection reuse, and especially for HTTP\u002F2 and gRPC where a single long-lived\nconnection carries every request, this means traffic pins to one pod.",[39,1393,1394],{},"This is why gRPC services behind a plain ClusterIP often show badly skewed load.\nThe fix is client-side load balancing, a proxy that understands HTTP\u002F2, or a\nservice mesh — not a different Service type.",[367,1396,1398],{"id":1397},"headless-services","Headless Services",[39,1400,1401,1402,1405],{},"Setting ",[247,1403,1404],{},"clusterIP: None"," disables the virtual IP entirely. DNS then returns the\npod IPs directly, one A record per ready endpoint:",[241,1407,1409],{"className":1164,"code":1408,"language":1166,"meta":46,"style":46},"spec:\n  clusterIP: None\n  selector:\n    app: cassandra\n",[247,1410,1411,1417,1427,1433],{"__ignoreMap":46},[250,1412,1413,1415],{"class":252,"line":56},[250,1414,1212],{"class":1173},[250,1416,657],{"class":263},[250,1418,1419,1422,1424],{"class":252,"line":203},[250,1420,1421],{"class":1173},"  clusterIP",[250,1423,1177],{"class":263},[250,1425,1426],{"class":278},"None\n",[250,1428,1429,1431],{"class":252,"line":47},[250,1430,1219],{"class":1173},[250,1432,657],{"class":263},[250,1434,1435,1437,1439],{"class":252,"line":290},[250,1436,1226],{"class":1173},[250,1438,1177],{"class":263},[250,1440,1441],{"class":278},"cassandra\n",[39,1443,1444],{},"This is what StatefulSets use. When each replica is individually addressable and\nidentity matters — database members, brokers, anything with a quorum — you want\nthe caller to see the real topology rather than a single virtual IP hiding it.",[477,1446,1447],{},"html pre.shiki code .sza-u, html code.shiki .sza-u{--shiki-default:#8DDB8C;--shiki-dark:#8DDB8C}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":47,"depth":47,"links":1449},[1450,1451,1452,1453,1454],{"id":1153,"depth":203,"text":1154},{"id":1275,"depth":203,"text":1276},{"id":1354,"depth":203,"text":1355},{"id":1381,"depth":203,"text":1382},{"id":1397,"depth":203,"text":1398},"ClusterIP, NodePort and LoadBalancer, and what each one actually allocates.",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fnetworking\u002Fservices",{"title":1136,"description":1455},"learn\u002Fen\u002Fcourses\u002Fnetworking\u002F01.services","6w0dHzL6jKSzV2gpGWIKGGQgiq8FUOgmMB__4BLHel8",{"id":1462,"title":1463,"access":34,"body":1464,"description":1740,"extension":51,"lang":110,"meta":1741,"navigation":24,"order":56,"partial":6,"path":1742,"seo":1743,"stem":1744,"__hash__":1745},"lessons\u002Flearn\u002Fen\u002Fcourses\u002Fpods\u002F01.the-pod.md","The pod",{"type":36,"value":1465,"toc":1735},[1466,1469,1472,1475,1479,1486,1617,1624,1629,1633,1636,1699,1702,1706,1726,1733],[234,1467,1463],{"id":1468},"the-pod",[39,1470,1471],{},"Kubernetes does not schedule containers. It schedules pods.",[39,1473,1474],{},"A pod is a group of containers that share a network namespace, share IPC, and can\nshare volumes. They are always placed on the same node, always started together,\nand always terminated together.",[367,1476,1478],{"id":1477},"what-sharing-a-network-namespace-means","What sharing a network namespace means",[39,1480,1481,1482,1485],{},"Every container in a pod sees the same IP address and the same port space. They\nreach each other over ",[247,1483,1484],{},"localhost",", and they cannot both bind the same port.",[241,1487,1489],{"className":1164,"code":1488,"language":1166,"meta":46,"style":46},"apiVersion: v1\nkind: Pod\nmetadata:\n  name: web\nspec:\n  containers:\n    - name: app\n      image: myapp:1.4\n      ports:\n        - containerPort: 8080\n    - name: metrics-proxy\n      image: proxy:2.0\n      ports:\n        - containerPort: 9090\n",[247,1490,1491,1499,1508,1514,1523,1529,1536,1548,1558,1565,1577,1588,1598,1605],{"__ignoreMap":46},[250,1492,1493,1495,1497],{"class":252,"line":56},[250,1494,1174],{"class":1173},[250,1496,1177],{"class":263},[250,1498,1180],{"class":278},[250,1500,1501,1503,1505],{"class":252,"line":203},[250,1502,1185],{"class":1173},[250,1504,1177],{"class":263},[250,1506,1507],{"class":278},"Pod\n",[250,1509,1510,1512],{"class":252,"line":47},[250,1511,1195],{"class":1173},[250,1513,657],{"class":263},[250,1515,1516,1518,1520],{"class":252,"line":290},[250,1517,1202],{"class":1173},[250,1519,1177],{"class":263},[250,1521,1522],{"class":278},"web\n",[250,1524,1525,1527],{"class":252,"line":296},[250,1526,1212],{"class":1173},[250,1528,657],{"class":263},[250,1530,1531,1534],{"class":252,"line":306},[250,1532,1533],{"class":1173},"  containers",[250,1535,657],{"class":263},[250,1537,1538,1540,1543,1545],{"class":252,"line":315},[250,1539,1242],{"class":263},[250,1541,1542],{"class":1173},"name",[250,1544,1177],{"class":263},[250,1546,1547],{"class":278},"app\n",[250,1549,1550,1553,1555],{"class":252,"line":329},[250,1551,1552],{"class":1173},"      image",[250,1554,1177],{"class":263},[250,1556,1557],{"class":278},"myapp:1.4\n",[250,1559,1560,1563],{"class":252,"line":334},[250,1561,1562],{"class":1173},"      ports",[250,1564,657],{"class":263},[250,1566,1567,1570,1573,1575],{"class":252,"line":339},[250,1568,1569],{"class":263},"        - ",[250,1571,1572],{"class":1173},"containerPort",[250,1574,1177],{"class":263},[250,1576,1260],{"class":389},[250,1578,1579,1581,1583,1585],{"class":252,"line":864},[250,1580,1242],{"class":263},[250,1582,1542],{"class":1173},[250,1584,1177],{"class":263},[250,1586,1587],{"class":278},"metrics-proxy\n",[250,1589,1591,1593,1595],{"class":252,"line":1590},12,[250,1592,1552],{"class":1173},[250,1594,1177],{"class":263},[250,1596,1597],{"class":278},"proxy:2.0\n",[250,1599,1601,1603],{"class":252,"line":1600},13,[250,1602,1562],{"class":1173},[250,1604,657],{"class":263},[250,1606,1608,1610,1612,1614],{"class":252,"line":1607},14,[250,1609,1569],{"class":263},[250,1611,1572],{"class":1173},[250,1613,1177],{"class":263},[250,1615,1616],{"class":389},"9090\n",[39,1618,1619,1620,1623],{},"The proxy scrapes ",[247,1621,1622],{},"http:\u002F\u002Flocalhost:8080"," — no service discovery, no DNS, no\nnetwork hop. This is the sidecar pattern, and the shared namespace is the whole\nreason it works.",[444,1625,1626],{},[39,1627,1628],{},"Two containers in one pod is a design decision, not a packaging convenience. The\nright question is whether they must scale together and die together. If either\nanswer is no, they belong in separate pods.",[367,1630,1632],{"id":1631},"init-containers","Init containers",[39,1634,1635],{},"Init containers run to completion, in order, before any app container starts. If\none fails, the pod restarts according to its policy and the sequence begins again.",[241,1637,1639],{"className":1164,"code":1638,"language":1166,"meta":46,"style":46},"spec:\n  initContainers:\n    - name: wait-for-db\n      image: busybox:1.36\n      command: ['sh', '-c', 'until nc -z db 5432; do sleep 1; done']\n",[247,1640,1641,1647,1654,1665,1674],{"__ignoreMap":46},[250,1642,1643,1645],{"class":252,"line":56},[250,1644,1212],{"class":1173},[250,1646,657],{"class":263},[250,1648,1649,1652],{"class":252,"line":203},[250,1650,1651],{"class":1173},"  initContainers",[250,1653,657],{"class":263},[250,1655,1656,1658,1660,1662],{"class":252,"line":47},[250,1657,1242],{"class":263},[250,1659,1542],{"class":1173},[250,1661,1177],{"class":263},[250,1663,1664],{"class":278},"wait-for-db\n",[250,1666,1667,1669,1671],{"class":252,"line":290},[250,1668,1552],{"class":1173},[250,1670,1177],{"class":263},[250,1672,1673],{"class":278},"busybox:1.36\n",[250,1675,1676,1679,1682,1685,1688,1691,1693,1696],{"class":252,"line":296},[250,1677,1678],{"class":1173},"      command",[250,1680,1681],{"class":263},": [",[250,1683,1684],{"class":278},"'sh'",[250,1686,1687],{"class":263},", ",[250,1689,1690],{"class":278},"'-c'",[250,1692,1687],{"class":263},[250,1694,1695],{"class":278},"'until nc -z db 5432; do sleep 1; done'",[250,1697,1698],{"class":263},"]\n",[39,1700,1701],{},"They are the correct place for migrations, waiting on a dependency, or fetching\nconfiguration — anything that must be finished before the app process begins.",[367,1703,1705],{"id":1704},"restart-policy-is-per-pod-not-per-container","Restart policy is per pod, not per container",[39,1707,1708,1711,1712,1687,1715,1718,1719,1722,1723,1725],{},[247,1709,1710],{},"restartPolicy"," applies to the pod as a whole: ",[247,1713,1714],{},"Always",[247,1716,1717],{},"OnFailure"," or ",[247,1720,1721],{},"Never",".\nDeployments require ",[247,1724,1714],{},". Jobs require one of the other two.",[39,1727,1728,1729,1732],{},"A crashing container is restarted in place by the kubelet, with exponential backoff\nup to five minutes. That backoff is what ",[247,1730,1731],{},"CrashLoopBackOff"," means — the pod is not\nbroken beyond repair, the kubelet is simply waiting before its next attempt.",[477,1734,1447],{},{"title":46,"searchDepth":47,"depth":47,"links":1736},[1737,1738,1739],{"id":1477,"depth":203,"text":1478},{"id":1631,"depth":203,"text":1632},{"id":1704,"depth":203,"text":1705},"Shared network and storage namespaces, and why the unit is not the container.",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fpods\u002Fthe-pod",{"title":1463,"description":1740},"learn\u002Fen\u002Fcourses\u002Fpods\u002F01.the-pod","MxEnNnEovq7oJ35S9_DH8RQtcRph-8E1rjzvVskgg5U",{"id":1747,"title":1748,"access":34,"body":1749,"description":2064,"extension":51,"lang":53,"meta":2065,"navigation":24,"order":203,"partial":6,"path":2066,"seo":2067,"stem":2068,"__hash__":2069},"lessons\u002Flearn\u002Fbn\u002Fcourses\u002Fevent-loop\u002F02.task-queues.md","টাস্ক কিউ",{"type":36,"value":1750,"toc":2059},[1751,1754,1757,1760,1766,1770,1773,1823,1830,1929,1945,1962,1970,1974,1977,2018,2025,2033,2037,2056],[234,1752,1748],{"id":1753},"টাস্ক-কিউ",[39,1755,1756],{},"ব্রাউজার কাজ শেষ করার পর কলব্যাকটা সরাসরি চালায় না। স্ট্যাক তখন ব্যস্ত থাকতে\nপারে। তাই কলব্যাকটা একটা কিউতে গিয়ে অপেক্ষা করে।",[39,1758,1759],{},"ইভেন্ট লুপের কাজ একটাই, আর সেটা খুব সহজ:",[1761,1762,1763],"blockquote",{},[39,1764,1765],{},"স্ট্যাক খালি? তাহলে কিউ থেকে একটা কাজ তুলে স্ট্যাকে বসাও।",[367,1767,1769],{"id":1768},"দুটো-কিউ-সমান-নয়","দুটো কিউ, সমান নয়",[39,1771,1772],{},"এখানেই বেশিরভাগ বিভ্রান্তি।",[1278,1774,1775,1788],{},[1281,1776,1777],{},[1284,1778,1779,1782,1785],{},[1287,1780,1781],{},"কিউ",[1287,1783,1784],{},"কারা যায়",[1287,1786,1787],{},"অগ্রাধিকার",[1297,1789,1790,1807],{},[1284,1791,1792,1795,1804],{},[1302,1793,1794],{},"মাইক্রোটাস্ক",[1302,1796,1797,1798,1687,1801],{},"প্রমিজ, ",[247,1799,1800],{},"queueMicrotask",[247,1802,1803],{},"MutationObserver",[1302,1805,1806],{},"বেশি",[1284,1808,1809,1812,1820],{},[1302,1810,1811],{},"ম্যাক্রোটাস্ক",[1302,1813,1814,1687,1816,1819],{},[247,1815,956],{},[247,1817,1818],{},"setInterval",", ক্লিক, নেটওয়ার্ক ইভেন্ট",[1302,1821,1822],{},"কম",[39,1824,1825,1826,1829],{},"স্ট্যাক খালি হলে ইভেন্ট লুপ ",[517,1827,1828],{},"পুরো মাইক্রোটাস্ক কিউ খালি করে",", তারপর একটা\nম্যাক্রোটাস্ক নেয়। একটা নয় — পুরোটা।",[241,1831,1833],{"className":243,"code":1832,"language":245,"meta":46,"style":46},"console.log('১')\n\nsetTimeout(() => console.log('২'), 0)\n\nPromise.resolve().then(() => console.log('৩'))\n\nconsole.log('৪')\n",[247,1834,1835,1848,1852,1875,1879,1912,1916],{"__ignoreMap":46},[250,1836,1837,1839,1841,1843,1846],{"class":252,"line":56},[250,1838,971],{"class":263},[250,1840,272],{"class":259},[250,1842,275],{"class":263},[250,1844,1845],{"class":278},"'১'",[250,1847,282],{"class":263},[250,1849,1850],{"class":252,"line":203},[250,1851,293],{"emptyLinePlaceholder":24},[250,1853,1854,1856,1858,1860,1862,1864,1866,1869,1871,1873],{"class":252,"line":47},[250,1855,956],{"class":259},[250,1857,987],{"class":263},[250,1859,990],{"class":255},[250,1861,993],{"class":263},[250,1863,272],{"class":259},[250,1865,275],{"class":263},[250,1867,1868],{"class":278},"'২'",[250,1870,1003],{"class":263},[250,1872,1006],{"class":389},[250,1874,282],{"class":263},[250,1876,1877],{"class":252,"line":290},[250,1878,293],{"emptyLinePlaceholder":24},[250,1880,1881,1884,1887,1890,1893,1896,1898,1900,1902,1904,1906,1909],{"class":252,"line":296},[250,1882,1883],{"class":389},"Promise",[250,1885,1886],{"class":263},".",[250,1888,1889],{"class":259},"resolve",[250,1891,1892],{"class":263},"().",[250,1894,1895],{"class":259},"then",[250,1897,987],{"class":263},[250,1899,990],{"class":255},[250,1901,993],{"class":263},[250,1903,272],{"class":259},[250,1905,275],{"class":263},[250,1907,1908],{"class":278},"'৩'",[250,1910,1911],{"class":263},"))\n",[250,1913,1914],{"class":252,"line":306},[250,1915,293],{"emptyLinePlaceholder":24},[250,1917,1918,1920,1922,1924,1927],{"class":252,"line":315},[250,1919,971],{"class":263},[250,1921,272],{"class":259},[250,1923,275],{"class":263},[250,1925,1926],{"class":278},"'৪'",[250,1928,282],{"class":263},[39,1930,1931,1932,1687,1935,1687,1938,1687,1941,1944],{},"আউটপুট: ",[247,1933,1934],{},"১",[247,1936,1937],{},"৪",[247,1939,1940],{},"৩",[247,1942,1943],{},"২","।",[39,1946,1947,1949,1950,1952,1953,1955,1956,1958,1959,1961],{},[247,1948,1934],{}," আর ",[247,1951,1937],{}," সরাসরি চলে, কারণ ওরা স্ট্যাকেই আছে। তারপর স্ট্যাক খালি হয়।\n",[247,1954,1940],{}," মাইক্রোটাস্ক, তাই আগে। ",[247,1957,1943],{}," ম্যাক্রোটাস্ক, তাই শেষে — ",[247,1960,1006],{}," মিলিসেকেন্ড\nলেখা থাকা সত্ত্বেও।",[540,1963,1964],{},[39,1965,1966,1969],{},[247,1967,1968],{},"setTimeout(fn, 0)"," মানে \"এখনই চালাও\" নয়। মানে \"কিউয়ের পেছনে দাঁড় করাও\"।\nসময়টা সর্বনিম্ন দেরি, নিশ্চিত দেরি নয়।",[367,1971,1973],{"id":1972},"মাইক্রোটাস্ক-দিয়ে-পেজ-আটকে-দেওয়া","মাইক্রোটাস্ক দিয়ে পেজ আটকে দেওয়া",[39,1975,1976],{},"যেহেতু লুপ পুরো মাইক্রোটাস্ক কিউ খালি করে তবেই এগোয়, একটা মাইক্রোটাস্ক যদি\nনিজেই আরেকটা মাইক্রোটাস্ক যোগ করতে থাকে, কিউ কখনো খালি হয় না:",[241,1978,1980],{"className":243,"code":1979,"language":245,"meta":46,"style":46},"function চিরকাল() {\n  Promise.resolve().then(চিরকাল)\n}\nচিরকাল()\n",[247,1981,1982,1991,2007,2011],{"__ignoreMap":46},[250,1983,1984,1986,1989],{"class":252,"line":56},[250,1985,256],{"class":255},[250,1987,1988],{"class":259}," চিরকাল",[250,1990,264],{"class":263},[250,1992,1993,1996,1998,2000,2002,2004],{"class":252,"line":203},[250,1994,1995],{"class":389},"  Promise",[250,1997,1886],{"class":263},[250,1999,1889],{"class":259},[250,2001,1892],{"class":263},[250,2003,1895],{"class":259},[250,2005,2006],{"class":263},"(চিরকাল)\n",[250,2008,2009],{"class":252,"line":47},[250,2010,287],{"class":263},[250,2012,2013,2016],{"class":252,"line":290},[250,2014,2015],{"class":259},"চিরকাল",[250,2017,312],{"class":263},[39,2019,2020,2021,2024],{},"পেজ জমে যাবে। ",[247,2022,2023],{},"while (true)","-এর মতোই, কিন্তু দেখতে নিরীহ — কারণ কোডে কোনো\nলুপ নেই।",[556,2026,2027],{},[39,2028,2029,2030,2032],{},"এই ভুলটা বাস্তবে ঘটে রিকার্সিভ প্রমিজ চেইনে — যেখানে থামার শর্তটা কখনো\nসত্যি হয় না। ",[247,2031,956],{}," দিয়ে একই জিনিস লিখলে পেজ বাঁচত, কারণ ম্যাক্রোটাস্কের\nমাঝে ব্রাউজার রেন্ডার করার সুযোগ পায়।",[367,2034,2036],{"id":2035},"মনে-রাখার-মতো","মনে রাখার মতো",[2038,2039,2040,2043,2046,2053],"ul",{},[514,2041,2042],{},"স্ট্যাক খালি না হলে কোনো কলব্যাক চলে না।",[514,2044,2045],{},"মাইক্রোটাস্ক সবসময় ম্যাক্রোটাস্কের আগে।",[514,2047,2048,2049,2052],{},"মাইক্রোটাস্ক কিউ ",[517,2050,2051],{},"পুরোটা"," খালি হয়, একটা করে নয়।",[514,2054,2055],{},"টাইমারের সময় মানে সর্বনিম্ন দেরি।",[477,2057,2058],{},"html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":47,"depth":47,"links":2060},[2061,2062,2063],{"id":1768,"depth":203,"text":1769},{"id":1972,"depth":203,"text":1973},{"id":2035,"depth":203,"text":2036},"ম্যাক্রোটাস্ক, মাইক্রোটাস্ক আর কোনটা আগে চলে।",{},"\u002Flearn\u002Fbn\u002Fcourses\u002Fevent-loop\u002Ftask-queues",{"title":1748,"description":2064},"learn\u002Fbn\u002Fcourses\u002Fevent-loop\u002F02.task-queues","9RU6CRf8BUDTOLqXJkwY2RnOFqCaUHLpBXRyoA9O8kM",{"id":2071,"title":2072,"access":102,"body":2073,"description":2302,"extension":51,"lang":110,"meta":2303,"navigation":24,"order":203,"partial":6,"path":2304,"seo":2305,"stem":2306,"__hash__":2307},"lessons\u002Flearn\u002Fen\u002Fcourses\u002Fcaching\u002F02.invalidation.md","Invalidation and stampedes",{"type":36,"value":2074,"toc":2296},[2075,2078,2081,2085,2088,2092,2095,2098,2101,2107,2113,2181,2187,2193,2197,2200,2239,2242,2245,2249,2252,2290,2293],[234,2076,2072],{"id":2077},"invalidation-and-stampedes",[39,2079,2080],{},"Caching is easy. Knowing when the cached value stopped being true is the hard part.",[367,2082,2084],{"id":2083},"ttl-is-a-guess-about-staleness","TTL is a guess about staleness",[39,2086,2087],{},"A TTL says \"I accept being wrong for up to this long\". Choosing one means deciding\nhow much staleness the product tolerates — a question for the product, not the\ninfrastructure.",[367,2089,2091],{"id":2090},"the-thundering-herd","The thundering herd",[39,2093,2094],{},"A popular key expires. A thousand concurrent requests miss simultaneously. All\nthousand hit the database with the same query.",[39,2096,2097],{},"The database, sized for the cached load, falls over. Recovery repopulates the cache,\nthe key expires again, and the pattern repeats on a cycle matching the TTL.",[39,2099,2100],{},"Three defences:",[39,2102,2103,2106],{},[517,2104,2105],{},"Locking."," The first miss takes a lock and recomputes; everyone else waits or\nserves stale. Correct, but adds a coordination point.",[39,2108,2109,2112],{},[517,2110,2111],{},"Probabilistic early expiry."," Recompute before expiry with a probability that\nrises as the TTL approaches, so refreshes spread out instead of synchronising.",[241,2114,2116],{"className":591,"code":2115,"language":593,"meta":46,"style":46},"def should_refresh(computed_at, ttl, beta=1.0):\n    age = now() - computed_at\n    return age + beta * delta * math.log(random.random()) >= ttl\n",[247,2117,2118,2136,2151],{"__ignoreMap":46},[250,2119,2120,2122,2125,2128,2130,2133],{"class":252,"line":56},[250,2121,600],{"class":255},[250,2123,2124],{"class":259}," should_refresh",[250,2126,2127],{"class":263},"(computed_at, ttl, beta",[250,2129,614],{"class":255},[250,2131,2132],{"class":389},"1.0",[250,2134,2135],{"class":263},"):\n",[250,2137,2138,2141,2143,2146,2148],{"class":252,"line":203},[250,2139,2140],{"class":263},"    age ",[250,2142,614],{"class":255},[250,2144,2145],{"class":263}," now() ",[250,2147,417],{"class":255},[250,2149,2150],{"class":263}," computed_at\n",[250,2152,2153,2155,2158,2161,2164,2167,2170,2172,2175,2178],{"class":252,"line":47},[250,2154,722],{"class":255},[250,2156,2157],{"class":263}," age ",[250,2159,2160],{"class":255},"+",[250,2162,2163],{"class":263}," beta ",[250,2165,2166],{"class":255},"*",[250,2168,2169],{"class":263}," delta ",[250,2171,2166],{"class":255},[250,2173,2174],{"class":263}," math.log(random.random()) ",[250,2176,2177],{"class":255},">=",[250,2179,2180],{"class":263}," ttl\n",[39,2182,2183,2186],{},[517,2184,2185],{},"Jitter."," Add randomness to every TTL so keys written together do not expire\ntogether. The cheapest fix and often sufficient.",[2188,2189,2190],"danger",{},[39,2191,2192],{},"Populating a cache during a deploy or a warm-up script writes thousands of keys\nwithin the same second. With a fixed TTL they all expire within the same second too,\nproducing a synchronised stampede hours later that looks completely unrelated to\nthe deploy that caused it. Always jitter.",[367,2194,2196],{"id":2195},"explicit-invalidation","Explicit invalidation",[39,2198,2199],{},"Delete the key on write:",[241,2201,2203],{"className":591,"code":2202,"language":593,"meta":46,"style":46},"def update_user(user_id, data):\n    db.update(user_id, data)\n    cache.delete(f\"user:{user_id}\")\n",[247,2204,2205,2215,2220],{"__ignoreMap":46},[250,2206,2207,2209,2212],{"class":252,"line":56},[250,2208,600],{"class":255},[250,2210,2211],{"class":259}," update_user",[250,2213,2214],{"class":263},"(user_id, data):\n",[250,2216,2217],{"class":252,"line":203},[250,2218,2219],{"class":263},"    db.update(user_id, data)\n",[250,2221,2222,2225,2227,2229,2231,2233,2235,2237],{"class":252,"line":47},[250,2223,2224],{"class":263},"    cache.delete(",[250,2226,620],{"class":255},[250,2228,623],{"class":278},[250,2230,626],{"class":255},[250,2232,629],{"class":263},[250,2234,632],{"class":255},[250,2236,635],{"class":278},[250,2238,282],{"class":263},[39,2240,2241],{},"Delete rather than overwrite. Overwriting introduces a race where two concurrent\nwriters leave the cache holding the older value.",[39,2243,2244],{},"The ordering matters too — database first, then cache. Reversed, a concurrent read\nbetween the two steps repopulates the cache with the old row and the stale value\nsurvives its TTL.",[367,2246,2248],{"id":2247},"cached-nothing","Cached nothing",[39,2250,2251],{},"The failure mode nobody instruments: a cache that works perfectly and is never hit,\nbecause keys are built from something that varies per request.",[241,2253,2255],{"className":591,"code":2254,"language":593,"meta":46,"style":46},"cache.set(f\"user:{user_id}:{request_id}\", user)  # unique every time\n",[247,2256,2257],{"__ignoreMap":46},[250,2258,2259,2262,2264,2266,2268,2270,2272,2275,2277,2280,2282,2284,2287],{"class":252,"line":56},[250,2260,2261],{"class":263},"cache.set(",[250,2263,620],{"class":255},[250,2265,623],{"class":278},[250,2267,626],{"class":255},[250,2269,629],{"class":263},[250,2271,632],{"class":255},[250,2273,2274],{"class":278},":",[250,2276,626],{"class":255},[250,2278,2279],{"class":263},"request_id",[250,2281,632],{"class":255},[250,2283,635],{"class":278},[250,2285,2286],{"class":263},", user)  ",[250,2288,2289],{"class":434},"# unique every time\n",[39,2291,2292],{},"Hit rate is zero. Latency is unchanged. Nothing errors. Monitor hit rate as a first\nclass metric, and alert when it drops — a cache silently doing nothing is a cost with\nno benefit.",[477,2294,2295],{},"html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}",{"title":46,"searchDepth":47,"depth":47,"links":2297},[2298,2299,2300,2301],{"id":2083,"depth":203,"text":2084},{"id":2090,"depth":203,"text":2091},{"id":2195,"depth":203,"text":2196},{"id":2247,"depth":203,"text":2248},"TTLs, explicit invalidation, and what happens when a hot key expires.",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fcaching\u002Finvalidation",{"title":2072,"description":2302},"learn\u002Fen\u002Fcourses\u002Fcaching\u002F02.invalidation","fahMLdo2hiDD5K_BQHGZCeYMOkIquLGdRIDAy0WkguI",{"id":2309,"title":2310,"access":34,"body":2311,"description":2711,"extension":51,"lang":110,"meta":2712,"navigation":24,"order":203,"partial":6,"path":2713,"seo":2714,"stem":2715,"__hash__":2716},"lessons\u002Flearn\u002Fen\u002Fcourses\u002Fevent-loop\u002F02.task-queues.md","Tasks and microtasks",{"type":36,"value":2312,"toc":2705},[2313,2316,2319,2366,2370,2377,2496,2499,2507,2510,2514,2519,2606,2619,2623,2626,2676,2687,2691,2694,2702],[234,2314,2310],{"id":2315},"tasks-and-microtasks",[39,2317,2318],{},"There is not one queue. There are two, and they are drained differently.",[1278,2320,2321,2334],{},[1281,2322,2323],{},[1284,2324,2325,2328,2331],{},[1287,2326,2327],{},"Queue",[1287,2329,2330],{},"Sources",[1287,2332,2333],{},"Drained",[1297,2335,2336,2351],{},[1284,2337,2338,2341,2348],{},[1302,2339,2340],{},"Task (macrotask)",[1302,2342,2343,1687,2345,2347],{},[247,2344,956],{},[247,2346,1818],{},", I\u002FO, events",[1302,2349,2350],{},"One per loop iteration",[1284,2352,2353,2356,2363],{},[1302,2354,2355],{},"Microtask",[1302,2357,2358,2359,1687,2361],{},"Promise callbacks, ",[247,2360,1800],{},[247,2362,1803],{},[1302,2364,2365],{},"Completely, after every task",[367,2367,2369],{"id":2368},"the-rule","The rule",[39,2371,2372,2373,2376],{},"After the stack empties, the event loop drains ",[517,2374,2375],{},"the entire microtask queue"," before\ntaking the next task. Microtasks added while draining are also processed in the same\npass.",[241,2378,2380],{"className":243,"code":2379,"language":245,"meta":46,"style":46},"console.log('script start')\n\nsetTimeout(() => console.log('timeout'), 0)\n\nPromise.resolve()\n  .then(() => console.log('promise 1'))\n  .then(() => console.log('promise 2'))\n\nconsole.log('script end')\n",[247,2381,2382,2395,2399,2422,2426,2436,2458,2479,2483],{"__ignoreMap":46},[250,2383,2384,2386,2388,2390,2393],{"class":252,"line":56},[250,2385,971],{"class":263},[250,2387,272],{"class":259},[250,2389,275],{"class":263},[250,2391,2392],{"class":278},"'script start'",[250,2394,282],{"class":263},[250,2396,2397],{"class":252,"line":203},[250,2398,293],{"emptyLinePlaceholder":24},[250,2400,2401,2403,2405,2407,2409,2411,2413,2416,2418,2420],{"class":252,"line":47},[250,2402,956],{"class":259},[250,2404,987],{"class":263},[250,2406,990],{"class":255},[250,2408,993],{"class":263},[250,2410,272],{"class":259},[250,2412,275],{"class":263},[250,2414,2415],{"class":278},"'timeout'",[250,2417,1003],{"class":263},[250,2419,1006],{"class":389},[250,2421,282],{"class":263},[250,2423,2424],{"class":252,"line":290},[250,2425,293],{"emptyLinePlaceholder":24},[250,2427,2428,2430,2432,2434],{"class":252,"line":296},[250,2429,1883],{"class":389},[250,2431,1886],{"class":263},[250,2433,1889],{"class":259},[250,2435,312],{"class":263},[250,2437,2438,2441,2443,2445,2447,2449,2451,2453,2456],{"class":252,"line":306},[250,2439,2440],{"class":263},"  .",[250,2442,1895],{"class":259},[250,2444,987],{"class":263},[250,2446,990],{"class":255},[250,2448,993],{"class":263},[250,2450,272],{"class":259},[250,2452,275],{"class":263},[250,2454,2455],{"class":278},"'promise 1'",[250,2457,1911],{"class":263},[250,2459,2460,2462,2464,2466,2468,2470,2472,2474,2477],{"class":252,"line":315},[250,2461,2440],{"class":263},[250,2463,1895],{"class":259},[250,2465,987],{"class":263},[250,2467,990],{"class":255},[250,2469,993],{"class":263},[250,2471,272],{"class":259},[250,2473,275],{"class":263},[250,2475,2476],{"class":278},"'promise 2'",[250,2478,1911],{"class":263},[250,2480,2481],{"class":252,"line":329},[250,2482,293],{"emptyLinePlaceholder":24},[250,2484,2485,2487,2489,2491,2494],{"class":252,"line":334},[250,2486,971],{"class":263},[250,2488,272],{"class":259},[250,2490,275],{"class":263},[250,2492,2493],{"class":278},"'script end'",[250,2495,282],{"class":263},[39,2497,2498],{},"Output:",[241,2500,2505],{"className":2501,"code":2503,"language":2504,"meta":46},[2502],"language-text","script start\nscript end\npromise 1\npromise 2\ntimeout\n","text",[247,2506,2503],{"__ignoreMap":46},[39,2508,2509],{},"Both promise callbacks run before the timeout, even though the timeout was scheduled\nfirst. They are microtasks; the timeout is a task.",[367,2511,2513],{"id":2512},"await-is-a-microtask-boundary","await is a microtask boundary",[39,2515,2516,2518],{},[247,2517,1113],{}," suspends the function and schedules the remainder as a promise continuation:",[241,2520,2522],{"className":243,"code":2521,"language":245,"meta":46,"style":46},"async function run() {\n  console.log('a')\n  await null \u002F\u002F still a microtask, even awaiting a non-promise\n  console.log('c')\n}\n\nrun()\nconsole.log('b')\n\u002F\u002F a, b, c\n",[247,2523,2524,2536,2549,2560,2573,2577,2581,2588,2601],{"__ignoreMap":46},[250,2525,2526,2528,2531,2534],{"class":252,"line":56},[250,2527,156],{"class":255},[250,2529,2530],{"class":255}," function",[250,2532,2533],{"class":259}," run",[250,2535,264],{"class":263},[250,2537,2538,2540,2542,2544,2547],{"class":252,"line":203},[250,2539,269],{"class":263},[250,2541,272],{"class":259},[250,2543,275],{"class":263},[250,2545,2546],{"class":278},"'a'",[250,2548,282],{"class":263},[250,2550,2551,2554,2557],{"class":252,"line":47},[250,2552,2553],{"class":255},"  await",[250,2555,2556],{"class":389}," null",[250,2558,2559],{"class":434}," \u002F\u002F still a microtask, even awaiting a non-promise\n",[250,2561,2562,2564,2566,2568,2571],{"class":252,"line":290},[250,2563,269],{"class":263},[250,2565,272],{"class":259},[250,2567,275],{"class":263},[250,2569,2570],{"class":278},"'c'",[250,2572,282],{"class":263},[250,2574,2575],{"class":252,"line":296},[250,2576,287],{"class":263},[250,2578,2579],{"class":252,"line":306},[250,2580,293],{"emptyLinePlaceholder":24},[250,2582,2583,2586],{"class":252,"line":315},[250,2584,2585],{"class":259},"run",[250,2587,312],{"class":263},[250,2589,2590,2592,2594,2596,2599],{"class":252,"line":329},[250,2591,971],{"class":263},[250,2593,272],{"class":259},[250,2595,275],{"class":263},[250,2597,2598],{"class":278},"'b'",[250,2600,282],{"class":263},[250,2602,2603],{"class":252,"line":334},[250,2604,2605],{"class":434},"\u002F\u002F a, b, c\n",[39,2607,2608,2611,2612,2614,2615,2618],{},[247,2609,2610],{},"await null"," does not skip the queue. Everything after the ",[247,2613,1113],{}," becomes a\nmicrotask, so the synchronous ",[247,2616,2617],{},"console.log('b')"," runs first.",[367,2620,2622],{"id":2621},"starving-the-loop","Starving the loop",[39,2624,2625],{},"Because microtasks drain completely, a microtask that schedules another microtask\nforever prevents any task from ever running:",[241,2627,2629],{"className":243,"code":2628,"language":245,"meta":46,"style":46},"function spin() {\n  Promise.resolve().then(spin)\n}\nspin()\n\u002F\u002F Timers never fire. Rendering never happens. The page is frozen —\n\u002F\u002F but the CPU profile looks like idle promise work, not a busy loop.\n",[247,2630,2631,2640,2655,2659,2666,2671],{"__ignoreMap":46},[250,2632,2633,2635,2638],{"class":252,"line":56},[250,2634,256],{"class":255},[250,2636,2637],{"class":259}," spin",[250,2639,264],{"class":263},[250,2641,2642,2644,2646,2648,2650,2652],{"class":252,"line":203},[250,2643,1995],{"class":389},[250,2645,1886],{"class":263},[250,2647,1889],{"class":259},[250,2649,1892],{"class":263},[250,2651,1895],{"class":259},[250,2653,2654],{"class":263},"(spin)\n",[250,2656,2657],{"class":252,"line":47},[250,2658,287],{"class":263},[250,2660,2661,2664],{"class":252,"line":290},[250,2662,2663],{"class":259},"spin",[250,2665,312],{"class":263},[250,2667,2668],{"class":252,"line":296},[250,2669,2670],{"class":434},"\u002F\u002F Timers never fire. Rendering never happens. The page is frozen —\n",[250,2672,2673],{"class":252,"line":306},[250,2674,2675],{"class":434},"\u002F\u002F but the CPU profile looks like idle promise work, not a busy loop.\n",[2188,2677,2678],{},[39,2679,2680,2681,2683,2684,2686],{},"This is worse than a ",[247,2682,2023],{}," loop, because it is harder to spot. The stack is\nshallow, each individual microtask is fast, and nothing looks obviously wrong. Use\n",[247,2685,956],{}," rather than promise recursion for anything that repeats indefinitely.",[367,2688,2690],{"id":2689},"rendering","Rendering",[39,2692,2693],{},"In a browser, rendering is neither a task nor a microtask — it happens between\ntasks, at most once per frame. Since microtasks drain before rendering, a long\nmicrotask chain delays paint just as effectively as blocking code.",[540,2695,2696],{},[39,2697,2698,2701],{},[247,2699,2700],{},"requestAnimationFrame"," callbacks run just before paint, after microtasks. That is\nthe correct place for visual updates, and the wrong place for anything expensive.",[477,2703,2704],{},"html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}",{"title":46,"searchDepth":47,"depth":47,"links":2706},[2707,2708,2709,2710],{"id":2368,"depth":203,"text":2369},{"id":2512,"depth":203,"text":2513},{"id":2621,"depth":203,"text":2622},{"id":2689,"depth":203,"text":2690},"Two queues, different draining rules, and the ordering that follows.",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fevent-loop\u002Ftask-queues",{"title":2310,"description":2711},"learn\u002Fen\u002Fcourses\u002Fevent-loop\u002F02.task-queues","Jzwld-KwT30H_KYGrTndaDQB3plJFyiXJ-0rWZCopTQ",{"id":2718,"title":2719,"access":34,"body":2720,"description":3131,"extension":51,"lang":110,"meta":3132,"navigation":24,"order":203,"partial":6,"path":3133,"seo":3134,"stem":3135,"__hash__":3136},"lessons\u002Flearn\u002Fen\u002Fcourses\u002Fnetworking\u002F02.ingress.md","Ingress",{"type":36,"value":2721,"toc":3124},[2722,2724,2729,2733,2740,2753,2757,2967,2973,2977,3024,3048,3053,3057,3100,3107,3112,3116,3119,3122],[234,2723,2719],{"id":224},[39,2725,1146,2726,2728],{},[247,2727,1336],{}," Service gives you one external IP per Service. Thirty services\nmeans thirty load balancers and thirty bills. Ingress exists to collapse that into\none entry point that routes by hostname and path.",[367,2730,2732],{"id":2731},"the-resource-is-only-a-declaration","The resource is only a declaration",[39,2734,2735,2736,2739],{},"This is the part that catches people out. An Ingress object is inert. It describes\nrouting rules; it does not implement them. Without an ",[517,2737,2738],{},"ingress controller"," running\nin the cluster — ingress-nginx, Traefik, HAProxy, a cloud-native one — creating an\nIngress does exactly nothing.",[556,2741,2742],{},[39,2743,2744,2745,2748,2749,2752],{},"An Ingress with no controller stays in a permanent pending state with no address,\nand produces no events explaining why. If ",[247,2746,2747],{},"kubectl get ingress"," shows an empty\n",[247,2750,2751],{},"ADDRESS"," column and nothing is logged, check whether a controller is installed\nbefore debugging the rules.",[367,2754,2756],{"id":2755},"a-minimal-rule-set","A minimal rule set",[241,2758,2760],{"className":1164,"code":2759,"language":1166,"meta":46,"style":46},"apiVersion: networking.k8s.io\u002Fv1\nkind: Ingress\nmetadata:\n  name: storefront\nspec:\n  ingressClassName: nginx\n  rules:\n    - host: shop.example.com\n      http:\n        paths:\n          - path: \u002Fapi\n            pathType: Prefix\n            backend:\n              service:\n                name: api\n                port:\n                  number: 80\n          - path: \u002F\n            pathType: Prefix\n            backend:\n              service:\n                name: web\n                port:\n                  number: 80\n",[247,2761,2762,2771,2780,2786,2795,2801,2811,2818,2830,2837,2844,2857,2867,2874,2881,2891,2899,2909,2921,2930,2936,2943,2952,2959],{"__ignoreMap":46},[250,2763,2764,2766,2768],{"class":252,"line":56},[250,2765,1174],{"class":1173},[250,2767,1177],{"class":263},[250,2769,2770],{"class":278},"networking.k8s.io\u002Fv1\n",[250,2772,2773,2775,2777],{"class":252,"line":203},[250,2774,1185],{"class":1173},[250,2776,1177],{"class":263},[250,2778,2779],{"class":278},"Ingress\n",[250,2781,2782,2784],{"class":252,"line":47},[250,2783,1195],{"class":1173},[250,2785,657],{"class":263},[250,2787,2788,2790,2792],{"class":252,"line":290},[250,2789,1202],{"class":1173},[250,2791,1177],{"class":263},[250,2793,2794],{"class":278},"storefront\n",[250,2796,2797,2799],{"class":252,"line":296},[250,2798,1212],{"class":1173},[250,2800,657],{"class":263},[250,2802,2803,2806,2808],{"class":252,"line":306},[250,2804,2805],{"class":1173},"  ingressClassName",[250,2807,1177],{"class":263},[250,2809,2810],{"class":278},"nginx\n",[250,2812,2813,2816],{"class":252,"line":315},[250,2814,2815],{"class":1173},"  rules",[250,2817,657],{"class":263},[250,2819,2820,2822,2825,2827],{"class":252,"line":329},[250,2821,1242],{"class":263},[250,2823,2824],{"class":1173},"host",[250,2826,1177],{"class":263},[250,2828,2829],{"class":278},"shop.example.com\n",[250,2831,2832,2835],{"class":252,"line":334},[250,2833,2834],{"class":1173},"      http",[250,2836,657],{"class":263},[250,2838,2839,2842],{"class":252,"line":339},[250,2840,2841],{"class":1173},"        paths",[250,2843,657],{"class":263},[250,2845,2846,2849,2852,2854],{"class":252,"line":864},[250,2847,2848],{"class":263},"          - ",[250,2850,2851],{"class":1173},"path",[250,2853,1177],{"class":263},[250,2855,2856],{"class":278},"\u002Fapi\n",[250,2858,2859,2862,2864],{"class":252,"line":1590},[250,2860,2861],{"class":1173},"            pathType",[250,2863,1177],{"class":263},[250,2865,2866],{"class":278},"Prefix\n",[250,2868,2869,2872],{"class":252,"line":1600},[250,2870,2871],{"class":1173},"            backend",[250,2873,657],{"class":263},[250,2875,2876,2879],{"class":252,"line":1607},[250,2877,2878],{"class":1173},"              service",[250,2880,657],{"class":263},[250,2882,2883,2886,2888],{"class":252,"line":83},[250,2884,2885],{"class":1173},"                name",[250,2887,1177],{"class":263},[250,2889,2890],{"class":278},"api\n",[250,2892,2894,2897],{"class":252,"line":2893},16,[250,2895,2896],{"class":1173},"                port",[250,2898,657],{"class":263},[250,2900,2902,2905,2907],{"class":252,"line":2901},17,[250,2903,2904],{"class":1173},"                  number",[250,2906,1177],{"class":263},[250,2908,1250],{"class":389},[250,2910,2912,2914,2916,2918],{"class":252,"line":2911},18,[250,2913,2848],{"class":263},[250,2915,2851],{"class":1173},[250,2917,1177],{"class":263},[250,2919,2920],{"class":278},"\u002F\n",[250,2922,2924,2926,2928],{"class":252,"line":2923},19,[250,2925,2861],{"class":1173},[250,2927,1177],{"class":263},[250,2929,2866],{"class":278},[250,2931,2932,2934],{"class":252,"line":50},[250,2933,2871],{"class":1173},[250,2935,657],{"class":263},[250,2937,2939,2941],{"class":252,"line":2938},21,[250,2940,2878],{"class":1173},[250,2942,657],{"class":263},[250,2944,2946,2948,2950],{"class":252,"line":2945},22,[250,2947,2885],{"class":1173},[250,2949,1177],{"class":263},[250,2951,1522],{"class":278},[250,2953,2955,2957],{"class":252,"line":2954},23,[250,2956,2896],{"class":1173},[250,2958,657],{"class":263},[250,2960,2961,2963,2965],{"class":252,"line":5},[250,2962,2904],{"class":1173},[250,2964,1177],{"class":263},[250,2966,1250],{"class":389},[39,2968,2969,2972],{},[247,2970,2971],{},"ingressClassName"," decides which controller claims this object. With two controllers\ninstalled and no class set, either both claim it or neither does — both outcomes are\nconfusing to debug.",[367,2974,2976],{"id":2975},"pathtype-matters-more-than-it-looks","pathType matters more than it looks",[1278,2978,2979,2989],{},[1281,2980,2981],{},[1284,2982,2983,2986],{},[1287,2984,2985],{},"Value",[1287,2987,2988],{},"Matches",[1297,2990,2991,3001,3014],{},[1284,2992,2993,2998],{},[1302,2994,2995],{},[247,2996,2997],{},"Exact",[1302,2999,3000],{},"The path string exactly, case-sensitive",[1284,3002,3003,3008],{},[1302,3004,3005],{},[247,3006,3007],{},"Prefix",[1302,3009,3010,3011,3013],{},"Split on ",[247,3012,1117],{},", element by element",[1284,3015,3016,3021],{},[1302,3017,3018],{},[247,3019,3020],{},"ImplementationSpecific",[1302,3022,3023],{},"Whatever the controller decides",[39,3025,3026,3028,3029,3032,3033,3036,3037,3039,3040,3043,3044,3047],{},[247,3027,3007],{}," compares path ",[947,3030,3031],{},"elements",", not characters. ",[247,3034,3035],{},"\u002Fapi"," matches ",[247,3038,3035],{}," and\n",[247,3041,3042],{},"\u002Fapi\u002Forders",", but not ",[247,3045,3046],{},"\u002Fapiary",". That distinction surprises people expecting a\nplain string prefix.",[39,3049,3050,3052],{},[247,3051,3020],{}," is where portability quietly dies — an ingress-nginx regex\npath will not survive a move to a different controller.",[367,3054,3056],{"id":3055},"tls","TLS",[241,3058,3060],{"className":1164,"code":3059,"language":1166,"meta":46,"style":46},"spec:\n  tls:\n    - hosts:\n        - shop.example.com\n      secretName: shop-tls\n",[247,3061,3062,3068,3075,3084,3090],{"__ignoreMap":46},[250,3063,3064,3066],{"class":252,"line":56},[250,3065,1212],{"class":1173},[250,3067,657],{"class":263},[250,3069,3070,3073],{"class":252,"line":203},[250,3071,3072],{"class":1173},"  tls",[250,3074,657],{"class":263},[250,3076,3077,3079,3082],{"class":252,"line":47},[250,3078,1242],{"class":263},[250,3080,3081],{"class":1173},"hosts",[250,3083,657],{"class":263},[250,3085,3086,3088],{"class":252,"line":290},[250,3087,1569],{"class":263},[250,3089,2829],{"class":278},[250,3091,3092,3095,3097],{"class":252,"line":296},[250,3093,3094],{"class":1173},"      secretName",[250,3096,1177],{"class":263},[250,3098,3099],{"class":278},"shop-tls\n",[39,3101,3102,3103,3106],{},"The Secret must be type ",[247,3104,3105],{},"kubernetes.io\u002Ftls",", must live in the same namespace as the\nIngress, and must already exist. A missing Secret does not block the Ingress from\nbeing created — it just serves the controller's default self-signed certificate,\nwhich looks like a certificate problem rather than a missing-object problem.",[540,3108,3109],{},[39,3110,3111],{},"In practice you rarely create these by hand. cert-manager watches Ingress objects\nand issues certificates automatically from an ACME issuer such as Let's Encrypt.",[367,3113,3115],{"id":3114},"when-ingress-is-not-enough","When Ingress is not enough",[39,3117,3118],{},"Ingress only models HTTP and HTTPS. Raw TCP, UDP, gRPC routing rules and weighted\ntraffic splits all sit outside the spec, which is why every controller grew its own\nannotations — and why those annotations do not port between controllers.",[39,3120,3121],{},"The Gateway API is the successor that models these properly, with separate resources\nfor infrastructure and routing. New clusters should look at it before committing to\ncontroller-specific annotations.",[477,3123,1447],{},{"title":46,"searchDepth":47,"depth":47,"links":3125},[3126,3127,3128,3129,3130],{"id":2731,"depth":203,"text":2732},{"id":2755,"depth":203,"text":2756},{"id":2975,"depth":203,"text":2976},{"id":3055,"depth":203,"text":3056},{"id":3114,"depth":203,"text":3115},"HTTP routing in front of Services, and why the resource does nothing on its own.",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fnetworking\u002Fingress",{"title":2719,"description":3131},"learn\u002Fen\u002Fcourses\u002Fnetworking\u002F02.ingress","ijcTN78jYH-ktpYMTtKJTvnbYvlGui3oJDXv_cjRfNA",{"id":3138,"title":3139,"access":34,"body":3140,"description":3415,"extension":51,"lang":110,"meta":3416,"navigation":24,"order":203,"partial":6,"path":3417,"seo":3418,"stem":3419,"__hash__":3420},"lessons\u002Flearn\u002Fen\u002Fcourses\u002Fpods\u002F02.controllers.md","Choosing a controller",{"type":36,"value":3141,"toc":3410},[3142,3145,3148,3230,3234,3237,3240,3244,3254,3257,3262,3266,3382,3395,3408],[234,3143,3139],{"id":3144},"choosing-a-controller",[39,3146,3147],{},"Each workload controller manages pods. They differ in what they promise about\nidentity, ordering and placement.",[1278,3149,3150,3163],{},[1281,3151,3152],{},[1284,3153,3154,3157,3160],{},[1287,3155,3156],{},"Controller",[1287,3158,3159],{},"Use when",[1287,3161,3162],{},"Guarantee",[1297,3164,3165,3178,3191,3204,3217],{},[1284,3166,3167,3172,3175],{},[1302,3168,3169],{},[247,3170,3171],{},"Deployment",[1302,3173,3174],{},"Stateless replicas",[1302,3176,3177],{},"Interchangeable pods, rolling updates",[1284,3179,3180,3185,3188],{},[1302,3181,3182],{},[247,3183,3184],{},"StatefulSet",[1302,3186,3187],{},"Stateful members",[1302,3189,3190],{},"Stable name, stable storage, ordered rollout",[1284,3192,3193,3198,3201],{},[1302,3194,3195],{},[247,3196,3197],{},"DaemonSet",[1302,3199,3200],{},"Node-level agents",[1302,3202,3203],{},"One pod per matching node",[1284,3205,3206,3211,3214],{},[1302,3207,3208],{},[247,3209,3210],{},"Job",[1302,3212,3213],{},"Run to completion",[1302,3215,3216],{},"Retries until the success count is met",[1284,3218,3219,3224,3227],{},[1302,3220,3221],{},[247,3222,3223],{},"CronJob",[1302,3225,3226],{},"Scheduled work",[1302,3228,3229],{},"Creates Jobs on a schedule",[367,3231,3233],{"id":3232},"deployments-give-you-no-identity","Deployments give you no identity",[39,3235,3236],{},"Deployment pods get random name suffixes and are replaced freely. Nothing about a\ngiven pod persists across a restart — not its name, not its storage, not its\nposition in a cluster.",[39,3238,3239],{},"This is exactly right for a web server and exactly wrong for a database replica.",[367,3241,3243],{"id":3242},"statefulsets-trade-speed-for-identity","StatefulSets trade speed for identity",[39,3245,3246,3247,1687,3250,3253],{},"A StatefulSet gives each pod an ordinal name (",[247,3248,3249],{},"db-0",[247,3251,3252],{},"db-1","), a stable DNS entry\nvia a headless Service, and its own PersistentVolumeClaim that survives rescheduling.",[39,3255,3256],{},"Rollouts happen one pod at a time, in reverse ordinal order, waiting for each to\nbecome ready before continuing.",[556,3258,3259],{},[39,3260,3261],{},"That serial rollout is a feature, not a limitation — but it means a StatefulSet\nwith fifty replicas and a slow readiness probe can take an hour to roll. Budget\nfor it before choosing StatefulSet for something that did not need identity.",[367,3263,3265],{"id":3264},"jobs-and-the-completion-count","Jobs and the completion count",[241,3267,3269],{"className":1164,"code":3268,"language":1166,"meta":46,"style":46},"apiVersion: batch\u002Fv1\nkind: Job\nmetadata:\n  name: migrate\nspec:\n  backoffLimit: 3\n  template:\n    spec:\n      restartPolicy: Never\n      containers:\n        - name: migrate\n          image: myapp:1.4\n          command: ['.\u002Fmigrate']\n",[247,3270,3271,3280,3289,3295,3304,3310,3320,3327,3334,3344,3351,3361,3370],{"__ignoreMap":46},[250,3272,3273,3275,3277],{"class":252,"line":56},[250,3274,1174],{"class":1173},[250,3276,1177],{"class":263},[250,3278,3279],{"class":278},"batch\u002Fv1\n",[250,3281,3282,3284,3286],{"class":252,"line":203},[250,3283,1185],{"class":1173},[250,3285,1177],{"class":263},[250,3287,3288],{"class":278},"Job\n",[250,3290,3291,3293],{"class":252,"line":47},[250,3292,1195],{"class":1173},[250,3294,657],{"class":263},[250,3296,3297,3299,3301],{"class":252,"line":290},[250,3298,1202],{"class":1173},[250,3300,1177],{"class":263},[250,3302,3303],{"class":278},"migrate\n",[250,3305,3306,3308],{"class":252,"line":296},[250,3307,1212],{"class":1173},[250,3309,657],{"class":263},[250,3311,3312,3315,3317],{"class":252,"line":306},[250,3313,3314],{"class":1173},"  backoffLimit",[250,3316,1177],{"class":263},[250,3318,3319],{"class":389},"3\n",[250,3321,3322,3325],{"class":252,"line":315},[250,3323,3324],{"class":1173},"  template",[250,3326,657],{"class":263},[250,3328,3329,3332],{"class":252,"line":329},[250,3330,3331],{"class":1173},"    spec",[250,3333,657],{"class":263},[250,3335,3336,3339,3341],{"class":252,"line":334},[250,3337,3338],{"class":1173},"      restartPolicy",[250,3340,1177],{"class":263},[250,3342,3343],{"class":278},"Never\n",[250,3345,3346,3349],{"class":252,"line":339},[250,3347,3348],{"class":1173},"      containers",[250,3350,657],{"class":263},[250,3352,3353,3355,3357,3359],{"class":252,"line":864},[250,3354,1569],{"class":263},[250,3356,1542],{"class":1173},[250,3358,1177],{"class":263},[250,3360,3303],{"class":278},[250,3362,3363,3366,3368],{"class":252,"line":1590},[250,3364,3365],{"class":1173},"          image",[250,3367,1177],{"class":263},[250,3369,1557],{"class":278},[250,3371,3372,3375,3377,3380],{"class":252,"line":1600},[250,3373,3374],{"class":1173},"          command",[250,3376,1681],{"class":263},[250,3378,3379],{"class":278},"'.\u002Fmigrate'",[250,3381,1698],{"class":263},[39,3383,3384,3387,3388,3391,3392,3394],{},[247,3385,3386],{},"backoffLimit"," counts failed pods, not failed containers. With\n",[247,3389,3390],{},"restartPolicy: Never"," a failure creates a new pod; with ",[247,3393,1717],{}," the container\nrestarts inside the existing pod. The two produce different pod counts for the same\nnumber of failures, which makes debugging confusing if you are not expecting it.",[540,3396,3397],{},[39,3398,3399,3400,3403,3404,3407],{},"Set ",[247,3401,3402],{},"ttlSecondsAfterFinished"," on Jobs. Without it, completed Job pods accumulate\nindefinitely and eventually make ",[247,3405,3406],{},"kubectl get pods"," unusable.",[477,3409,1447],{},{"title":46,"searchDepth":47,"depth":47,"links":3411},[3412,3413,3414],{"id":3232,"depth":203,"text":3233},{"id":3242,"depth":203,"text":3243},{"id":3264,"depth":203,"text":3265},"Deployment, StatefulSet, DaemonSet and Job — what each one actually guarantees.",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fpods\u002Fcontrollers",{"title":3139,"description":3415},"learn\u002Fen\u002Fcourses\u002Fpods\u002F02.controllers","VSCelnVYA4VGJd0fBOn99YCc4llCWJ8uDyfJB4DOFKM",{"id":3422,"title":3423,"access":34,"body":3424,"description":3786,"extension":51,"lang":110,"meta":3787,"navigation":24,"order":47,"partial":6,"path":3788,"seo":3789,"stem":3790,"__hash__":3791},"lessons\u002Flearn\u002Fen\u002Fcourses\u002Fnetworking\u002F03.network-policies.md","Network policies",{"type":36,"value":3425,"toc":3780},[3426,3429,3432,3438,3442,3449,3584,3591,3615,3619,3629,3675,3687,3691,3694,3703,3707,3710,3769,3778],[234,3427,3423],{"id":3428},"network-policies",[39,3430,3431],{},"By default, every pod in a cluster can reach every other pod, in any namespace.\nThe flat network is a deliberate simplification — and a poor security posture the\nmoment you run more than one team's workloads.",[39,3433,3434,3437],{},[247,3435,3436],{},"NetworkPolicy"," narrows it.",[367,3439,3441],{"id":3440},"selecting-turns-on-default-deny","Selecting turns on default-deny",[39,3443,3444,3445,3448],{},"A NetworkPolicy does not add rules to a permissive baseline. The moment any policy\nselects a pod, that pod switches to default-deny ",[517,3446,3447],{},"for the directions the policy\nmentions",", and only the listed rules are permitted.",[241,3450,3452],{"className":1164,"code":3451,"language":1166,"meta":46,"style":46},"apiVersion: networking.k8s.io\u002Fv1\nkind: NetworkPolicy\nmetadata:\n  name: payments-ingress\nspec:\n  podSelector:\n    matchLabels:\n      app: payments\n  policyTypes: [Ingress]\n  ingress:\n    - from:\n        - podSelector:\n            matchLabels:\n              app: checkout\n      ports:\n        - port: 8080\n",[247,3453,3454,3462,3471,3477,3486,3492,3499,3506,3515,3526,3533,3542,3551,3558,3568,3574],{"__ignoreMap":46},[250,3455,3456,3458,3460],{"class":252,"line":56},[250,3457,1174],{"class":1173},[250,3459,1177],{"class":263},[250,3461,2770],{"class":278},[250,3463,3464,3466,3468],{"class":252,"line":203},[250,3465,1185],{"class":1173},[250,3467,1177],{"class":263},[250,3469,3470],{"class":278},"NetworkPolicy\n",[250,3472,3473,3475],{"class":252,"line":47},[250,3474,1195],{"class":1173},[250,3476,657],{"class":263},[250,3478,3479,3481,3483],{"class":252,"line":290},[250,3480,1202],{"class":1173},[250,3482,1177],{"class":263},[250,3484,3485],{"class":278},"payments-ingress\n",[250,3487,3488,3490],{"class":252,"line":296},[250,3489,1212],{"class":1173},[250,3491,657],{"class":263},[250,3493,3494,3497],{"class":252,"line":306},[250,3495,3496],{"class":1173},"  podSelector",[250,3498,657],{"class":263},[250,3500,3501,3504],{"class":252,"line":315},[250,3502,3503],{"class":1173},"    matchLabels",[250,3505,657],{"class":263},[250,3507,3508,3511,3513],{"class":252,"line":329},[250,3509,3510],{"class":1173},"      app",[250,3512,1177],{"class":263},[250,3514,1207],{"class":278},[250,3516,3517,3520,3522,3524],{"class":252,"line":334},[250,3518,3519],{"class":1173},"  policyTypes",[250,3521,1681],{"class":263},[250,3523,2719],{"class":278},[250,3525,1698],{"class":263},[250,3527,3528,3531],{"class":252,"line":339},[250,3529,3530],{"class":1173},"  ingress",[250,3532,657],{"class":263},[250,3534,3535,3537,3540],{"class":252,"line":864},[250,3536,1242],{"class":263},[250,3538,3539],{"class":1173},"from",[250,3541,657],{"class":263},[250,3543,3544,3546,3549],{"class":252,"line":1590},[250,3545,1569],{"class":263},[250,3547,3548],{"class":1173},"podSelector",[250,3550,657],{"class":263},[250,3552,3553,3556],{"class":252,"line":1600},[250,3554,3555],{"class":1173},"            matchLabels",[250,3557,657],{"class":263},[250,3559,3560,3563,3565],{"class":252,"line":1607},[250,3561,3562],{"class":1173},"              app",[250,3564,1177],{"class":263},[250,3566,3567],{"class":278},"checkout\n",[250,3569,3570,3572],{"class":252,"line":83},[250,3571,1562],{"class":1173},[250,3573,657],{"class":263},[250,3575,3576,3578,3580,3582],{"class":252,"line":2893},[250,3577,1569],{"class":263},[250,3579,1245],{"class":1173},[250,3581,1177],{"class":263},[250,3583,1260],{"class":389},[39,3585,3586,3587,3590],{},"Payments now accepts traffic from checkout on 8080 and nothing else. Egress is\nuntouched, because ",[247,3588,3589],{},"policyTypes"," does not list it.",[556,3592,3593],{},[39,3594,3595,3596,3599,3600,3602,3603,3606,3607,3610,3611,3614],{},"Listing ",[247,3597,3598],{},"Egress"," in ",[247,3601,3589],{}," with an empty ",[247,3604,3605],{},"egress"," block denies ",[947,3608,3609],{},"all","\noutbound traffic — including DNS to ",[247,3612,3613],{},"kube-dns",". Name resolution stops, so every\noutbound connection fails with a resolution error rather than a connection error,\nand the policy looks unrelated to the symptom. Always allow UDP and TCP 53 to the\nDNS namespace when you start restricting egress.",[367,3616,3618],{"id":3617},"namespace-selectors-need-labels","Namespace selectors need labels",[39,3620,3621,3624,3625,3628],{},[247,3622,3623],{},"namespaceSelector"," matches on namespace labels, not names. Namespaces have no\nuseful labels by default beyond ",[247,3626,3627],{},"kubernetes.io\u002Fmetadata.name",", which the control\nplane sets automatically:",[241,3630,3632],{"className":1164,"code":3631,"language":1166,"meta":46,"style":46},"ingress:\n  - from:\n      - namespaceSelector:\n          matchLabels:\n            kubernetes.io\u002Fmetadata.name: monitoring\n",[247,3633,3634,3640,3649,3658,3665],{"__ignoreMap":46},[250,3635,3636,3638],{"class":252,"line":56},[250,3637,224],{"class":1173},[250,3639,657],{"class":263},[250,3641,3642,3645,3647],{"class":252,"line":203},[250,3643,3644],{"class":263},"  - ",[250,3646,3539],{"class":1173},[250,3648,657],{"class":263},[250,3650,3651,3654,3656],{"class":252,"line":47},[250,3652,3653],{"class":263},"      - ",[250,3655,3623],{"class":1173},[250,3657,657],{"class":263},[250,3659,3660,3663],{"class":252,"line":290},[250,3661,3662],{"class":1173},"          matchLabels",[250,3664,657],{"class":263},[250,3666,3667,3670,3672],{"class":252,"line":296},[250,3668,3669],{"class":1173},"            kubernetes.io\u002Fmetadata.name",[250,3671,1177],{"class":263},[250,3673,3674],{"class":278},"monitoring\n",[39,3676,3677,3678,3680,3681,3683,3684,3686],{},"Combining ",[247,3679,3623],{}," and ",[247,3682,3548],{}," in a single ",[247,3685,3539],{}," entry is an AND.\nListing them as two entries is an OR. One character of YAML indentation separates\n\"Prometheus pods in the monitoring namespace\" from \"anything in monitoring, plus\nPrometheus pods anywhere\".",[367,3688,3690],{"id":3689},"the-cni-has-to-implement-it","The CNI has to implement it",[39,3692,3693],{},"Like Ingress, the object is only a declaration. Calico, Cilium and Antrea enforce\npolicies. Flannel, on its own, does not.",[2188,3695,3696],{},[39,3697,3698,3699,3702],{},"Applying NetworkPolicy on a CNI that ignores it produces no error and no warning.\n",[247,3700,3701],{},"kubectl get networkpolicy"," lists the object, the API accepted it, and traffic\nflows exactly as before. A policy you believe is enforced but is not is worse than\nno policy — verify by actually attempting a connection that should be denied.",[367,3704,3706],{"id":3705},"a-sensible-starting-point","A sensible starting point",[39,3708,3709],{},"Deny everything inbound in a namespace, then open specific paths:",[241,3711,3713],{"className":1164,"code":3712,"language":1166,"meta":46,"style":46},"apiVersion: networking.k8s.io\u002Fv1\nkind: NetworkPolicy\nmetadata:\n  name: default-deny-ingress\nspec:\n  podSelector: {}\n  policyTypes: [Ingress]\n",[247,3714,3715,3723,3731,3737,3746,3752,3759],{"__ignoreMap":46},[250,3716,3717,3719,3721],{"class":252,"line":56},[250,3718,1174],{"class":1173},[250,3720,1177],{"class":263},[250,3722,2770],{"class":278},[250,3724,3725,3727,3729],{"class":252,"line":203},[250,3726,1185],{"class":1173},[250,3728,1177],{"class":263},[250,3730,3470],{"class":278},[250,3732,3733,3735],{"class":252,"line":47},[250,3734,1195],{"class":1173},[250,3736,657],{"class":263},[250,3738,3739,3741,3743],{"class":252,"line":290},[250,3740,1202],{"class":1173},[250,3742,1177],{"class":263},[250,3744,3745],{"class":278},"default-deny-ingress\n",[250,3747,3748,3750],{"class":252,"line":296},[250,3749,1212],{"class":1173},[250,3751,657],{"class":263},[250,3753,3754,3756],{"class":252,"line":306},[250,3755,3496],{"class":1173},[250,3757,3758],{"class":263},": {}\n",[250,3760,3761,3763,3765,3767],{"class":252,"line":315},[250,3762,3519],{"class":1173},[250,3764,1681],{"class":263},[250,3766,2719],{"class":278},[250,3768,1698],{"class":263},[39,3770,3771,3772,3774,3775,3777],{},"An empty ",[247,3773,3548],{}," selects every pod in the namespace. With no ",[247,3776,224],{}," rules,\nnothing is allowed in. Layer permissive policies on top per service — policies are\nadditive, so any rule that allows traffic wins over the baseline denial.",[477,3779,1447],{},{"title":46,"searchDepth":47,"depth":47,"links":3781},[3782,3783,3784,3785],{"id":3440,"depth":203,"text":3441},{"id":3617,"depth":203,"text":3618},{"id":3689,"depth":203,"text":3690},{"id":3705,"depth":203,"text":3706},"Narrowing a default-allow flat network, and the traps in doing it.",{},"\u002Flearn\u002Fen\u002Fcourses\u002Fnetworking\u002Fnetwork-policies",{"title":3423,"description":3786},"learn\u002Fen\u002Fcourses\u002Fnetworking\u002F03.network-policies","6JfTFK0E4FUQ3up_YPMrK33esdPME_tZFDmROIyTNDw",[3793,3826,3856,3880,3904,3929,3953,3978,4000,4024,4046,4068,4091,4115,4138,4161,4185,4208,4230,4253,4276,4298,4321,4343],{"id":3794,"title":3795,"access":102,"author":3796,"body":3797,"cover":3804,"coverAlt":3805,"currency":3806,"description":3807,"extension":51,"featured":6,"file":3808,"lang":53,"meta":3809,"navigation":24,"pages":3810,"path":3811,"previewPages":61,"price":61,"seo":3812,"sku":61,"stem":3813,"subjects":3814,"tags":3816,"type":3823,"updated":3824,"__hash__":3825},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fdevops-cloud-engineering.md","DevOps ও Cloud Engineering — সম্পূর্ণ প্র্যাকটিক্যাল গাইড","Mohin Uddin",{"type":36,"value":3798,"toc":3802},[3799],[39,3800,3801],{},"Git থেকে Kubernetes, Terraform আর observability পর্যন্ত — শুরু থেকে Job-Ready হওয়ার ২২ চ্যাপ্টারের প্র্যাকটিক্যাল বাংলা হ্যান্ডবুক। প্রতিটা টুলের ইনস্টলেশন, স্টেপ-বাই-স্টেপ কনফিগারেশন আর বাস্তব কমান্ড সহ, AWS, GCP ও Azure কভার করে।",{"title":46,"searchDepth":47,"depth":47,"links":3803},[],"\u002Fcovers\u002Fdevops-cloud-engineering.png","Cover of DevOps ও Cloud Engineering — সম্পূর্ণ প্র্যাকটিক্যাল গাইড","GBP","Git থেকে Kubernetes, Terraform আর observability পর্যন্ত — শুরু থেকে Job-Ready হওয়ার ২২ চ্যাপ্টারের প্র্যাকটিক্যাল বাংলা হ্যান্ডবুক।","devops-cloud-engineering.pdf",{},78,"\u002Flearn\u002Fbn\u002Fbooks\u002Fdevops-cloud-engineering",{"title":3795,"description":3807},"learn\u002Fbn\u002Fbooks\u002Fdevops-cloud-engineering",[3815,183],"devops",[3815,3817,3818,183,3819,3820,3821,3822],"cloud","docker","terraform","ci-cd","gitops","observability","book","2026-08-23","TGHcLSlw2oLBQ_P2uIMs0RKSLeJgH6mzugtSonoPnsE",{"id":3827,"title":3828,"access":102,"author":3796,"body":3829,"cover":3836,"coverAlt":3837,"currency":3806,"description":3838,"extension":51,"featured":6,"file":3839,"lang":53,"meta":3840,"navigation":24,"pages":3841,"path":3842,"previewPages":61,"price":61,"seo":3843,"sku":61,"stem":3844,"subjects":3845,"tags":3847,"type":3823,"updated":3824,"__hash__":3855},"books\u002Flearn\u002Fbn\u002Fbooks\u002Ffull-stack-roadmap.md","ফুল স্ট্যাক ডেভেলপার হওয়ার রোডম্যাপ",{"type":36,"value":3830,"toc":3834},[3831],[39,3832,3833],{},"HTML\u002FCSS\u002FJS থেকে শুরু করে Vue, Nuxt, Node.js আর Express দিয়ে প্রফেশনাল ফুল স্ট্যাক ডেভেলপার হওয়ার সম্পূর্ণ বাংলা গাইড — কোন ধাপে কী শিখতে হবে আর কেন, সেই ক্রম ধরে সাজানো।",{"title":46,"searchDepth":47,"depth":47,"links":3835},[],"\u002Fcovers\u002Ffull-stack-roadmap.png","Cover of ফুল স্ট্যাক ডেভেলপার হওয়ার রোডম্যাপ","HTML\u002FCSS\u002FJS থেকে শুরু করে Vue, Nuxt, Node.js আর Express দিয়ে প্রফেশনাল ফুল স্ট্যাক ডেভেলপার হওয়ার সম্পূর্ণ বাংলা গাইড।","full-stack-roadmap.pdf",{},104,"\u002Flearn\u002Fbn\u002Fbooks\u002Ffull-stack-roadmap",{"title":3828,"description":3838},"learn\u002Fbn\u002Fbooks\u002Ffull-stack-roadmap",[3846,64,92],"web-development",[3848,64,3849,3850,3851,3852,3853,3854],"full-stack","vue","nuxt","node","express","postgresql","mongodb","_k8J_zYTjRB9svB6wHL1H2bCWvKyusRwjXXjzL7vFi4",{"id":3857,"title":3858,"access":102,"author":3796,"body":3859,"cover":3866,"coverAlt":3867,"currency":3806,"description":3863,"extension":51,"featured":6,"file":3868,"lang":53,"meta":3869,"navigation":24,"pages":3870,"path":3871,"previewPages":61,"price":61,"seo":3872,"sku":61,"stem":3873,"subjects":3874,"tags":3875,"type":3823,"updated":3878,"__hash__":3879},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fcicd-automation-frontend.md","CI\u002FCD ও অটোমেশন for Frontend",{"type":36,"value":3860,"toc":3864},[3861],[39,3862,3863],{},"Vue, Nuxt আর Hugo প্রজেক্টের জন্য সম্পূর্ণ Continuous Integration ও Deployment পাইপলাইন তৈরির প্র্যাক্টিক্যাল গাইড — বিল্ড থেকে প্রোডাকশন পর্যন্ত।",{"title":46,"searchDepth":47,"depth":47,"links":3865},[],"\u002Fcovers\u002Fcicd-automation-frontend.png","Cover of CI\u002FCD ও অটোমেশন for Frontend","cicd-automation-frontend.pdf",{},103,"\u002Flearn\u002Fbn\u002Fbooks\u002Fcicd-automation-frontend",{"title":3858,"description":3863},"learn\u002Fbn\u002Fbooks\u002Fcicd-automation-frontend",[3815,3846],[3820,3876,3821,3877,3849,3850],"github-actions","automation","2026-08-19","_KnSgN8sLG9sSEEQ23Hqs3Q1wJgEG-kkx3qWutpBVhw",{"id":3881,"title":3882,"access":102,"author":3796,"body":3883,"cover":3890,"coverAlt":3891,"currency":3806,"description":3887,"extension":51,"featured":6,"file":3892,"lang":53,"meta":3893,"navigation":24,"pages":3894,"path":3895,"previewPages":61,"price":61,"seo":3896,"sku":61,"stem":3897,"subjects":3898,"tags":3899,"type":3823,"updated":3878,"__hash__":3903},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fdocker-containers.md","ডকার ও কনটেইনার — বেসিক থেকে প্রোডাকশন",{"type":36,"value":3884,"toc":3888},[3885],[39,3886,3887],{},"একটা সাধারণ অ্যাপ্লিকেশন হাতে-কলমে তৈরি করে, সেটাকে ধাপে ধাপে containerize করে, শেষে multi-container, প্রোডাকশন-রেডি সিস্টেম পর্যন্ত নিয়ে যাওয়ার সম্পূর্ণ প্র্যাক্টিক্যাল গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":3889},[],"\u002Fcovers\u002Fdocker-containers.png","Cover of ডকার ও কনটেইনার — বেসিক থেকে প্রোডাকশন","docker-containers.pdf",{},101,"\u002Flearn\u002Fbn\u002Fbooks\u002Fdocker-containers",{"title":3882,"description":3887},"learn\u002Fbn\u002Fbooks\u002Fdocker-containers",[3815],[3818,3900,3901,3902],"containers","dockerfile","docker-compose","NStsQa4K6DOtPaItZRS38Xzl-NQ1KajKG5xGSTOafl8",{"id":3905,"title":3906,"access":102,"author":3796,"body":3907,"cover":3914,"coverAlt":3915,"currency":3806,"description":3911,"extension":51,"featured":6,"file":3916,"lang":53,"meta":3917,"navigation":24,"pages":3918,"path":3919,"previewPages":61,"price":61,"seo":3920,"sku":61,"stem":3921,"subjects":3922,"tags":3923,"type":3823,"updated":3878,"__hash__":3928},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fharvester-kubernetes-kubedb.md","হার্ভেস্টার থেকে KubeDB পর্যন্ত",{"type":36,"value":3908,"toc":3912},[3909],[39,3910,3911],{},"Harvester HCI দিয়ে ভার্চুয়াল মেশিন ও Kubernetes ক্লাস্টার তৈরি করে, তার উপর KubeDB দিয়ে প্রোডাকশন-গ্রেড ডাটাবেজ চালানোর সম্পূর্ণ প্র্যাক্টিক্যাল গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":3913},[],"\u002Fcovers\u002Fharvester-kubernetes-kubedb.png","Cover of হার্ভেস্টার থেকে KubeDB পর্যন্ত","harvester-kubernetes-kubedb.pdf",{},99,"\u002Flearn\u002Fbn\u002Fbooks\u002Fharvester-kubernetes-kubedb",{"title":3906,"description":3911},"learn\u002Fbn\u002Fbooks\u002Fharvester-kubernetes-kubedb",[3815,183],[3924,3925,183,3926,3927],"harvester","hci","kubedb","rke2","jEj0zBGOLF8VydMi2YDNVhy9u-lxsyuAXzn1RLZs0eg",{"id":3930,"title":3931,"access":102,"author":3796,"body":3932,"cover":3939,"coverAlt":3940,"currency":3806,"description":3936,"extension":51,"featured":6,"file":3941,"lang":53,"meta":3942,"navigation":24,"pages":3943,"path":3944,"previewPages":61,"price":61,"seo":3945,"sku":61,"stem":3946,"subjects":3947,"tags":3949,"type":3823,"updated":3878,"__hash__":3952},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fkeno-ei-design.md","কেন এই design?",{"type":36,"value":3933,"toc":3937},[3934],[39,3935,3936],{},"কী design করতে হয় তা না। কেন সেটা কাজ করে, সেটা শেখো।",{"title":46,"searchDepth":47,"depth":47,"links":3938},[],"\u002Fcovers\u002Fkeno-ei-design.png","Cover of কেন এই design?","keno-ei-design.pdf",{},130,"\u002Flearn\u002Fbn\u002Fbooks\u002Fkeno-ei-design",{"title":3931,"description":3936},"learn\u002Fbn\u002Fbooks\u002Fkeno-ei-design",[3948],"design",[3948,3950,3951],"ux","design-reasoning","_hOGpeI7-budI3fxPP63lLWsf8PT9Kc8KGsbUvTAPPE",{"id":3954,"title":3955,"access":102,"author":3796,"body":3956,"cover":3963,"coverAlt":3964,"currency":3806,"description":3960,"extension":51,"featured":6,"file":3965,"lang":53,"meta":3966,"navigation":24,"pages":3967,"path":3968,"previewPages":61,"price":61,"seo":3969,"sku":61,"stem":3970,"subjects":3971,"tags":3973,"type":3823,"updated":3878,"__hash__":3977},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fprompt-na-context.md","প্রম্পট না, কনটেক্সট",{"type":36,"value":3957,"toc":3961},[3958],[39,3959,3960],{},"AI কোথায় সাহায্য করে, কোথায় করে না — একজন Design Engineer-এর নোটবুক।",{"title":46,"searchDepth":47,"depth":47,"links":3962},[],"\u002Fcovers\u002Fprompt-na-context.png","Cover of প্রম্পট না, কনটেক্সট","prompt-na-context.pdf",{},76,"\u002Flearn\u002Fbn\u002Fbooks\u002Fprompt-na-context",{"title":3955,"description":3960},"learn\u002Fbn\u002Fbooks\u002Fprompt-na-context",[3972,3948],"ai",[3972,3974,3975,3976],"prompt-engineering","context","notebook","rDd_EYkCWOSZiicCr0DjYkeNfUvNJUdGuW4bIL0DPao",{"id":3979,"title":3980,"access":102,"author":3796,"body":3981,"cover":3988,"coverAlt":3989,"currency":3806,"description":3985,"extension":51,"featured":6,"file":3990,"lang":53,"meta":3991,"navigation":24,"pages":108,"path":3992,"previewPages":61,"price":61,"seo":3993,"sku":61,"stem":3994,"subjects":3995,"tags":3996,"type":3823,"updated":3878,"__hash__":3999},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fsystem-designer-to-ceo.md","সিস্টেম ডিজাইনার থেকে CEO",{"type":36,"value":3982,"toc":3986},[3983],[39,3984,3985],{},"একজন Design Engineer থেকে System Designer, Director, আর CEO পর্যন্ত ক্যারিয়ার জার্নির একটা সংক্ষিপ্ত, প্র্যাক্টিক্যাল রোডম্যাপ — কোন স্কিল, কোন বই, আর কোন অভ্যাস কাজে লাগবে প্রতিটা ধাপে।",{"title":46,"searchDepth":47,"depth":47,"links":3987},[],"\u002Fcovers\u002Fsystem-designer-to-ceo.png","Cover of সিস্টেম ডিজাইনার থেকে CEO","system-designer-to-ceo.pdf",{},"\u002Flearn\u002Fbn\u002Fbooks\u002Fsystem-designer-to-ceo",{"title":3980,"description":3985},"learn\u002Fbn\u002Fbooks\u002Fsystem-designer-to-ceo",[92],[92,3997,124,3998],"leadership","strategy","g4PaDeWVOd4SiCF7lrr3Tk5LjQHKqF6_dOO2JyKKaEQ",{"id":4001,"title":4002,"access":102,"author":3796,"body":4003,"cover":4010,"coverAlt":4011,"currency":3806,"description":4007,"extension":51,"featured":6,"file":4012,"lang":53,"meta":4013,"navigation":24,"pages":4014,"path":4015,"previewPages":61,"price":61,"seo":4016,"sku":61,"stem":4017,"subjects":4018,"tags":4019,"type":3823,"updated":190,"__hash__":4023},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fcloudflare.md","Cloudflare বাংলা গাইড",{"type":36,"value":4004,"toc":4008},[4005],[39,4006,4007],{},"Cloudflare ব্যবহারের সম্পূর্ণ বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4009},[],"\u002Fcovers\u002Fcloudflare.png","Cover of Cloudflare বাংলা গাইড","cloudflare.pdf",{},174,"\u002Flearn\u002Fbn\u002Fbooks\u002Fcloudflare",{"title":4002,"description":4007},"learn\u002Fbn\u002Fbooks\u002Fcloudflare",[3846,3815],[4020,4021,4022],"cloudflare","cdn","dns","a_5N5HfoEy5XPi6pUX1Qz25i9MUSpBND2GfMC7OfeLc",{"id":4025,"title":4026,"access":102,"author":3796,"body":4027,"cover":4034,"coverAlt":4035,"currency":3806,"description":4031,"extension":51,"featured":6,"file":4036,"lang":53,"meta":4037,"navigation":24,"pages":3918,"path":4038,"previewPages":61,"price":61,"seo":4039,"sku":61,"stem":4040,"subjects":4041,"tags":4042,"type":3823,"updated":4044,"__hash__":4045},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fcloud-app-ux.md","ক্লাউড অ্যাপ UX গাইড",{"type":36,"value":4028,"toc":4032},[4029],[39,4030,4031],{},"ক্লাউড অ্যাপ্লিকেশনের ইন্টারফেস ডিজাইন নিয়ে বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4033},[],"\u002Fcovers\u002Fcloud-app-ux.png","Cover of ক্লাউড অ্যাপ UX গাইড","cloud-app-ux.pdf",{},"\u002Flearn\u002Fbn\u002Fbooks\u002Fcloud-app-ux",{"title":4026,"description":4031},"learn\u002Fbn\u002Fbooks\u002Fcloud-app-ux",[3948],[3950,3817,4043],"interface","2026-08-02","kGVly3QbcAPZwvDhCn0NC0Bx1tUyLNGzcN7kFCEWWi0",{"id":4047,"title":4048,"access":102,"author":3796,"body":4049,"cover":4056,"coverAlt":4057,"currency":3806,"description":4053,"extension":51,"featured":6,"file":4058,"lang":53,"meta":4059,"navigation":24,"pages":4060,"path":4061,"previewPages":61,"price":61,"seo":4062,"sku":61,"stem":4063,"subjects":4064,"tags":4065,"type":3823,"updated":4066,"__hash__":4067},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fkubernetes-shikhi.md","কুবারনেটিস শিখি — বেসিক থেকে অ্যাডভান্স",{"type":36,"value":4050,"toc":4054},[4051],[39,4052,4053],{},"বেসিক থেকে অ্যাডভান্স পর্যন্ত কুবারনেটিস শেখার বাংলা বই।",{"title":46,"searchDepth":47,"depth":47,"links":4055},[],"\u002Fcovers\u002Fkubernetes-shikhi.png","Cover of কুবারনেটিস শিখি — বেসিক থেকে অ্যাডভান্স","kubernetes-shikhi.pdf",{},162,"\u002Flearn\u002Fbn\u002Fbooks\u002Fkubernetes-shikhi",{"title":4048,"description":4053},"learn\u002Fbn\u002Fbooks\u002Fkubernetes-shikhi",[183],[183,186,222],"2026-07-25","zaEfhfROnnRfLQ7NwycbYa1Mi3DMijQR72h8DEt5KVg",{"id":4069,"title":4070,"access":102,"author":3796,"body":4071,"cover":4078,"coverAlt":4079,"currency":3806,"description":4075,"extension":51,"featured":6,"file":4080,"lang":53,"meta":4081,"navigation":24,"pages":4082,"path":4083,"previewPages":61,"price":61,"seo":4084,"sku":61,"stem":4085,"subjects":4086,"tags":4087,"type":3823,"updated":4089,"__hash__":4090},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fdaily-javascript.md","ডেইলি জাভাস্ক্রিপ্ট গাইড",{"type":36,"value":4072,"toc":4076},[4073],[39,4074,4075],{},"প্রতিদিন অল্প অল্প করে জাভাস্ক্রিপ্ট চর্চার বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4077},[],"\u002Fcovers\u002Fdaily-javascript.png","Cover of ডেইলি জাভাস্ক্রিপ্ট গাইড","daily-javascript.pdf",{},113,"\u002Flearn\u002Fbn\u002Fbooks\u002Fdaily-javascript",{"title":4070,"description":4075},"learn\u002Fbn\u002Fbooks\u002Fdaily-javascript",[64],[64,4088],"practice","2026-07-12","ENlNDbufLnHKH4O6t_SUxBBzPhLXhvJqCnyHIcVv6dQ",{"id":4092,"title":4093,"access":102,"author":3796,"body":4094,"cover":4101,"coverAlt":4102,"currency":3806,"description":4098,"extension":51,"featured":6,"file":4103,"lang":53,"meta":4104,"navigation":24,"pages":4105,"path":4106,"previewPages":61,"price":61,"seo":4107,"sku":61,"stem":4108,"subjects":4109,"tags":4110,"type":3823,"updated":4089,"__hash__":4114},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fhono.md","Hono বাংলা গাইড",{"type":36,"value":4095,"toc":4099},[4096],[39,4097,4098],{},"Hono ফ্রেমওয়ার্ক দিয়ে ওয়েব API তৈরির বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4100},[],"\u002Fcovers\u002Fhono.png","Cover of Hono বাংলা গাইড","hono.pdf",{},88,"\u002Flearn\u002Fbn\u002Fbooks\u002Fhono",{"title":4093,"description":4098},"learn\u002Fbn\u002Fbooks\u002Fhono",[3846,64],[4111,4112,4113],"hono","api","typescript","YxN_cOsGyX9N27XmlfER5hlY6JNbfpT71-fHDyS0GFE",{"id":4116,"title":4117,"access":102,"author":3796,"body":4118,"cover":4125,"coverAlt":4126,"currency":3806,"description":4122,"extension":51,"featured":6,"file":4127,"lang":53,"meta":4128,"navigation":24,"pages":140,"path":4129,"previewPages":61,"price":61,"seo":4130,"sku":61,"stem":4131,"subjects":4132,"tags":4133,"type":4135,"updated":4136,"__hash__":4137},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fkubedb-interview.md","KubeDB ইন্টারভিউ গাইড",{"type":36,"value":4119,"toc":4123},[4120],[39,4121,4122],{},"KubeDB নিয়ে ইন্টারভিউ প্রস্তুতির সংক্ষিপ্ত নোট।",{"title":46,"searchDepth":47,"depth":47,"links":4124},[],"\u002Fcovers\u002Fkubedb-interview.png","Cover of KubeDB ইন্টারভিউ গাইড","kubedb-interview.pdf",{},"\u002Flearn\u002Fbn\u002Fbooks\u002Fkubedb-interview",{"title":4117,"description":4122},"learn\u002Fbn\u002Fbooks\u002Fkubedb-interview",[183,92],[3926,4134],"interview","notes","2026-07-07","FXbq3rk6aKgQ8ROCLonbh9goPAsf4t5yExnbHBotmS0",{"id":4139,"title":4140,"access":102,"author":3796,"body":4141,"cover":4148,"coverAlt":4149,"currency":3806,"description":4145,"extension":51,"featured":6,"file":4150,"lang":53,"meta":4151,"navigation":24,"pages":4152,"path":4153,"previewPages":61,"price":61,"seo":4154,"sku":61,"stem":4155,"subjects":4156,"tags":4157,"type":4135,"updated":4159,"__hash__":4160},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fhono-project-ideas.md","Hono প্রজেক্ট আইডিয়া",{"type":36,"value":4142,"toc":4146},[4143],[39,4144,4145],{},"Hono দিয়ে বানানো যায় এমন প্রজেক্টের আইডিয়া সংকলন।",{"title":46,"searchDepth":47,"depth":47,"links":4147},[],"\u002Fcovers\u002Fhono-project-ideas.png","Cover of Hono প্রজেক্ট আইডিয়া","hono-project-ideas.pdf",{},47,"\u002Flearn\u002Fbn\u002Fbooks\u002Fhono-project-ideas",{"title":4140,"description":4145},"learn\u002Fbn\u002Fbooks\u002Fhono-project-ideas",[3846],[4111,4158],"projects","2026-07-05","xUiWsw5XfcZettj9sAhX6rQK-OYKBiD8mYMs8b1vug4",{"id":4162,"title":4163,"access":102,"author":3796,"body":4164,"cover":4171,"coverAlt":4172,"currency":3806,"description":4168,"extension":51,"featured":6,"file":4173,"lang":53,"meta":4174,"navigation":24,"pages":4175,"path":4176,"previewPages":61,"price":61,"seo":4177,"sku":61,"stem":4178,"subjects":4179,"tags":4180,"type":3823,"updated":4183,"__hash__":4184},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fcomputer-graphics-animation.md","কম্পিউটার গ্রাফিক্স ও অ্যানিমেশন",{"type":36,"value":4165,"toc":4169},[4166],[39,4167,4168],{},"কম্পিউটার গ্রাফিক্স ও অ্যানিমেশনের মূল ধারণা নিয়ে বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4170},[],"\u002Fcovers\u002Fcomputer-graphics-animation.png","Cover of কম্পিউটার গ্রাফিক্স ও অ্যানিমেশন","computer-graphics-animation.pdf",{},109,"\u002Flearn\u002Fbn\u002Fbooks\u002Fcomputer-graphics-animation",{"title":4163,"description":4168},"learn\u002Fbn\u002Fbooks\u002Fcomputer-graphics-animation",[3948],[4181,4182],"graphics","animation","2026-07-04","12ioXJmJskDgERFNjlF74gVhxadOn4THbtA6eMCsIfg",{"id":4186,"title":4187,"access":102,"author":3796,"body":4188,"cover":4195,"coverAlt":4196,"currency":3806,"description":4192,"extension":51,"featured":6,"file":4197,"lang":53,"meta":4198,"navigation":24,"pages":4199,"path":4200,"previewPages":61,"price":61,"seo":4201,"sku":61,"stem":4202,"subjects":4203,"tags":4204,"type":3823,"updated":4206,"__hash__":4207},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fautomation-shikhi-hategolome.md","অটোমেশন শিখি হাতে-কলমে",{"type":36,"value":4189,"toc":4193},[4190],[39,4191,4192],{},"হাতে-কলমে অটোমেশন শেখার বাংলা বই — উদাহরণ ধরে ধরে এগোনো।",{"title":46,"searchDepth":47,"depth":47,"links":4194},[],"\u002Fcovers\u002Fautomation-shikhi-hategolome.png","Cover of অটোমেশন শিখি হাতে-কলমে","automation-shikhi-hategolome.pdf",{},102,"\u002Flearn\u002Fbn\u002Fbooks\u002Fautomation-shikhi-hategolome",{"title":4187,"description":4192},"learn\u002Fbn\u002Fbooks\u002Fautomation-shikhi-hategolome",[3815],[3877,4205,3815],"scripting","2026-07-03","PA0t1-IvTrbkUkepNIhbKwdRm9hvwywahDqGWXAHMzs",{"id":4209,"title":4210,"access":102,"author":3796,"body":4211,"cover":4218,"coverAlt":4219,"currency":3806,"description":4215,"extension":51,"featured":6,"file":4220,"lang":53,"meta":4221,"navigation":24,"pages":4222,"path":4223,"previewPages":61,"price":61,"seo":4224,"sku":61,"stem":4225,"subjects":4226,"tags":4228,"type":3823,"updated":4206,"__hash__":4229},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fcybersecurity.md","সাইবারসিকিউরিটি বাংলা গাইড",{"type":36,"value":4212,"toc":4216},[4213],[39,4214,4215],{},"সাইবারসিকিউরিটির মৌলিক বিষয়গুলো নিয়ে বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4217},[],"\u002Fcovers\u002Fcybersecurity.png","Cover of সাইবারসিকিউরিটি বাংলা গাইড","cybersecurity.pdf",{},96,"\u002Flearn\u002Fbn\u002Fbooks\u002Fcybersecurity",{"title":4210,"description":4215},"learn\u002Fbn\u002Fbooks\u002Fcybersecurity",[4227],"security",[4227,222],"Qf52TsXKVd33aMzqg9CGgpQwYIjjwkEbFA47Ht4u6JE",{"id":4231,"title":4232,"access":102,"author":3796,"body":4233,"cover":4240,"coverAlt":4241,"currency":3806,"description":4237,"extension":51,"featured":6,"file":4242,"lang":53,"meta":4243,"navigation":24,"pages":3943,"path":4244,"previewPages":61,"price":61,"seo":4245,"sku":61,"stem":4246,"subjects":4247,"tags":4248,"type":3823,"updated":4251,"__hash__":4252},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fappscode-toolkit.md","AppsCode টুলকিট গাইড",{"type":36,"value":4234,"toc":4238},[4235],[39,4236,4237],{},"AppsCode-এর টুলকিট নিয়ে বাংলায় লেখা গাইড — ইনস্টলেশন থেকে দৈনন্দিন ব্যবহার পর্যন্ত।",{"title":46,"searchDepth":47,"depth":47,"links":4239},[],"\u002Fcovers\u002Fappscode-toolkit.png","Cover of AppsCode টুলকিট গাইড","appscode-toolkit.pdf",{},"\u002Flearn\u002Fbn\u002Fbooks\u002Fappscode-toolkit",{"title":4232,"description":4237},"learn\u002Fbn\u002Fbooks\u002Fappscode-toolkit",[3815,183],[4249,183,4250],"appscode","tooling","2026-07-01","dj8hSWl1W5-yU25Jaj3kGJxaP2gF0jbmv_zg670zLeI",{"id":4254,"title":4255,"access":102,"author":3796,"body":4256,"cover":4263,"coverAlt":4264,"currency":3806,"description":4260,"extension":51,"featured":6,"file":4265,"lang":53,"meta":4266,"navigation":24,"pages":4267,"path":4268,"previewPages":61,"price":61,"seo":4269,"sku":61,"stem":4270,"subjects":4271,"tags":4272,"type":3823,"updated":4251,"__hash__":4275},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fkubedb-ui-design.md","KubeDB UI ডিজাইন গাইড",{"type":36,"value":4257,"toc":4261},[4258],[39,4259,4260],{},"KubeDB-র ইন্টারফেস ডিজাইন নিয়ে বিস্তারিত বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4262},[],"\u002Fcovers\u002Fkubedb-ui-design.png","Cover of KubeDB UI ডিজাইন গাইড","kubedb-ui-design.pdf",{},281,"\u002Flearn\u002Fbn\u002Fbooks\u002Fkubedb-ui-design",{"title":4255,"description":4260},"learn\u002Fbn\u002Fbooks\u002Fkubedb-ui-design",[3948,183],[4273,4274,3926],"ui","design-system","5kH12e7qXAmMLoVX6MdFOyw58WLE6RKrQ9c_IhO13ZY",{"id":4277,"title":4278,"access":102,"author":3796,"body":4279,"cover":4286,"coverAlt":4287,"currency":3806,"description":4283,"extension":51,"featured":6,"file":4288,"lang":53,"meta":4289,"navigation":24,"pages":4290,"path":4291,"previewPages":61,"price":61,"seo":4292,"sku":61,"stem":4293,"subjects":4294,"tags":4296,"type":3823,"updated":4251,"__hash__":4297},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fkubernetes-kubedb.md","Kubernetes ও KubeDB",{"type":36,"value":4280,"toc":4284},[4281],[39,4282,4283],{},"Kubernetes আর KubeDB একসাথে ব্যবহারের বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4285},[],"\u002Fcovers\u002Fkubernetes-kubedb.png","Cover of Kubernetes ও KubeDB","kubernetes-kubedb.pdf",{},234,"\u002Flearn\u002Fbn\u002Fbooks\u002Fkubernetes-kubedb",{"title":4278,"description":4283},"learn\u002Fbn\u002Fbooks\u002Fkubernetes-kubedb",[183,4295],"databases",[183,3926],"1-rumxjf4jJeTS0UgFre9W8pKbe0NcSDqmaIMTveKs0",{"id":4299,"title":4300,"access":102,"author":3796,"body":4301,"cover":4308,"coverAlt":4309,"currency":3806,"description":4305,"extension":51,"featured":6,"file":4310,"lang":53,"meta":4311,"navigation":24,"pages":4312,"path":4313,"previewPages":61,"price":61,"seo":4314,"sku":61,"stem":4315,"subjects":4316,"tags":4317,"type":3823,"updated":4251,"__hash__":4320},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fstash-kubestash.md","Stash ও KubeStash গাইড",{"type":36,"value":4302,"toc":4306},[4303],[39,4304,4305],{},"Kubernetes-এ ব্যাকআপ ও রিস্টোরের জন্য Stash এবং KubeStash-এর বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4307},[],"\u002Fcovers\u002Fstash-kubestash.png","Cover of Stash ও KubeStash গাইড","stash-kubestash.pdf",{},175,"\u002Flearn\u002Fbn\u002Fbooks\u002Fstash-kubestash",{"title":4300,"description":4305},"learn\u002Fbn\u002Fbooks\u002Fstash-kubestash",[183,3815],[4318,4319,183],"stash","backup","o6v9uMzDAt-g2e8wEQoriy9fm2MzEmVmG_BAfU3Pr68",{"id":4322,"title":4323,"access":102,"author":3796,"body":4324,"cover":4331,"coverAlt":4332,"currency":3806,"description":4328,"extension":51,"featured":6,"file":4333,"lang":53,"meta":4334,"navigation":24,"pages":4335,"path":4336,"previewPages":61,"price":61,"seo":4337,"sku":61,"stem":4338,"subjects":4339,"tags":4340,"type":3823,"updated":4341,"__hash__":4342},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fkubedb.md","KubeDB বাংলা গাইড",{"type":36,"value":4325,"toc":4329},[4326],[39,4327,4328],{},"Kubernetes-এ ডেটাবেস চালানোর জন্য KubeDB-র সম্পূর্ণ বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4330},[],"\u002Fcovers\u002Fkubedb.png","Cover of KubeDB বাংলা গাইড","kubedb.pdf",{},225,"\u002Flearn\u002Fbn\u002Fbooks\u002Fkubedb",{"title":4323,"description":4328},"learn\u002Fbn\u002Fbooks\u002Fkubedb",[183,4295],[3926,4295,183],"2026-06-29","oT7Ra0OIH1TruoGG_XtkyhXGLwa1fSwot-qBzzWOI5M",{"id":4344,"title":4345,"access":102,"author":3796,"body":4346,"cover":4353,"coverAlt":4354,"currency":3806,"description":4350,"extension":51,"featured":6,"file":4355,"lang":53,"meta":4356,"navigation":24,"pages":4357,"path":4358,"previewPages":61,"price":61,"seo":4359,"sku":61,"stem":4360,"subjects":4361,"tags":4362,"type":3823,"updated":4341,"__hash__":4365},"books\u002Flearn\u002Fbn\u002Fbooks\u002Fproduct-design-process.md","প্রোডাক্ট ডিজাইন প্রসেস",{"type":36,"value":4347,"toc":4351},[4348],[39,4349,4350],{},"প্রোডাক্ট ডিজাইনের ধাপে ধাপে প্রসেস নিয়ে বাংলা গাইড।",{"title":46,"searchDepth":47,"depth":47,"links":4352},[],"\u002Fcovers\u002Fproduct-design-process.png","Cover of প্রোডাক্ট ডিজাইন প্রসেস","product-design-process.pdf",{},106,"\u002Flearn\u002Fbn\u002Fbooks\u002Fproduct-design-process",{"title":4345,"description":4350},"learn\u002Fbn\u002Fbooks\u002Fproduct-design-process",[3948],[4363,4364],"product-design","process","ojdejKTRzfZrLZU_5ggrk1OYpM3tF6e86cZbbmtztg0",[4367,4487,4567,4715,4829,4862,4926],{"id":4368,"title":4369,"body":4370,"date":70,"description":4477,"extension":51,"lang":53,"meta":4478,"minutes":47,"navigation":24,"path":4479,"seo":4480,"stem":4481,"subjects":4482,"tags":4483,"__hash__":4486},"articles\u002Flearn\u002Fbn\u002Farticles\u002F2026-08-06.md","`==` কেন `0 == \"0\"` সত্যি বলে",{"type":36,"value":4371,"toc":4475},[4372,4386,4400,4403,4444,4453,4472],[39,4373,4374,4377,4378,4381,4382,4385],{},[247,4375,4376],{},"0 == '0'"," সত্যি, কিন্তু ",[247,4379,4380],{},"0 === '0'"," মিথ্যা। কারণ ",[247,4383,4384],{},"=="," তুলনার আগে দুই পাশকে\nএক টাইপে আনার চেষ্টা করে।",[39,4387,4388,4389,4392,4393,4395,4396,4399],{},"নিয়মটা হলো: এক পাশ সংখ্যা আর অন্য পাশ স্ট্রিং হলে, স্ট্রিংটাকে সংখ্যা বানানো\nহয়। তাই ",[247,4390,4391],{},"'0'"," হয়ে যায় ",[247,4394,1006],{},", আর ",[247,4397,4398],{},"0 == 0"," স্বাভাবিকভাবেই সত্যি।",[39,4401,4402],{},"মজার ব্যাপার হলো এটা ট্রানজিটিভ নয়:",[241,4404,4406],{"className":243,"code":4405,"language":245,"meta":46,"style":46},"0 == '' \u002F\u002F true\n0 == '0' \u002F\u002F true\n'' == '0' \u002F\u002F false\n",[247,4407,4408,4421,4432],{"__ignoreMap":46},[250,4409,4410,4412,4415,4418],{"class":252,"line":56},[250,4411,1006],{"class":389},[250,4413,4414],{"class":255}," ==",[250,4416,4417],{"class":278}," ''",[250,4419,4420],{"class":434}," \u002F\u002F true\n",[250,4422,4423,4425,4427,4430],{"class":252,"line":203},[250,4424,1006],{"class":389},[250,4426,4414],{"class":255},[250,4428,4429],{"class":278}," '0'",[250,4431,4420],{"class":434},[250,4433,4434,4437,4439,4441],{"class":252,"line":47},[250,4435,4436],{"class":278},"''",[250,4438,4414],{"class":255},[250,4440,4429],{"class":278},[250,4442,4443],{"class":434}," \u002F\u002F false\n",[39,4445,4446,4447,4449,4450,4452],{},"প্রথম দুটোয় স্ট্রিং সংখ্যায় রূপান্তরিত হয়। তৃতীয়টায় দুই পাশই স্ট্রিং, তাই\nকোনো রূপান্তর হয় না — আর ",[247,4448,4436],{}," কখনো ",[247,4451,4391],{},"-এর সমান নয়।",[39,4454,4455,4456,4459,4460,4463,4464,4467,4468,4471],{},"তাই ",[247,4457,4458],{},"==="," ব্যবহার করুন। ব্যতিক্রম একটাই: ",[247,4461,4462],{},"x == null"," লিখলে ",[247,4465,4466],{},"null"," আর\n",[247,4469,4470],{},"undefined"," দুটোই এক সাথে ধরা পড়ে, যেটা মাঝে মাঝে আসলেই যা দরকার।",[477,4473,4474],{},"html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":47,"depth":47,"links":4476},[],"টাইপ কোয়ার্শনের নিয়মটা অদ্ভুত নয়, শুধু লেখা থাকে না।",{},"\u002Flearn\u002Fbn\u002Farticles\u002F2026-08-06",{"title":4369,"description":4477},"learn\u002Fbn\u002Farticles\u002F2026-08-06",[64],[4484,4485],"জাভাস্ক্রিপ্ট","টাইপ","Mc9K8rtXOcLJvBY7LR3rHgeDNj-YN6dabTYJPB14e0w",{"id":4488,"title":4489,"body":4490,"date":70,"description":4558,"extension":51,"lang":110,"meta":4559,"minutes":203,"navigation":24,"path":4560,"seo":4561,"stem":4562,"subjects":4563,"tags":4564,"__hash__":4566},"articles\u002Flearn\u002Fen\u002Farticles\u002F2026-08-06.md","A ClusterIP cannot be pinged",{"type":36,"value":4491,"toc":4556},[4492,4495,4498,4525,4528,4550,4553],[39,4493,4494],{},"A ClusterIP is not assigned to any network interface. Nothing owns it, so nothing\nanswers ICMP.",[39,4496,4497],{},"What actually exists is a set of iptables or IPVS rules on every node that rewrite\nthe destination address — but only for the ports the Service declares.",[241,4499,4503],{"className":4500,"code":4501,"language":4502,"meta":46,"style":46},"language-bash shiki shiki-themes github-dark-dimmed github-dark-dimmed","ping 10.96.0.42          # always fails, means nothing\ncurl 10.96.0.42:80       # this is the real test\n","bash",[247,4504,4505,4515],{"__ignoreMap":46},[250,4506,4507,4509,4512],{"class":252,"line":56},[250,4508,1373],{"class":709},[250,4510,4511],{"class":389}," 10.96.0.42",[250,4513,4514],{"class":434},"          # always fails, means nothing\n",[250,4516,4517,4519,4522],{"class":252,"line":203},[250,4518,1377],{"class":709},[250,4520,4521],{"class":278}," 10.96.0.42:80",[250,4523,4524],{"class":434},"       # this is the real test\n",[39,4526,4527],{},"If you need to know whether a Service is wired up, check its endpoints instead:",[241,4529,4531],{"className":4500,"code":4530,"language":4502,"meta":46,"style":46},"kubectl get endpointslices -l kubernetes.io\u002Fservice-name=payments\n",[247,4532,4533],{"__ignoreMap":46},[250,4534,4535,4538,4541,4544,4547],{"class":252,"line":56},[250,4536,4537],{"class":709},"kubectl",[250,4539,4540],{"class":278}," get",[250,4542,4543],{"class":278}," endpointslices",[250,4545,4546],{"class":389}," -l",[250,4548,4549],{"class":278}," kubernetes.io\u002Fservice-name=payments\n",[39,4551,4552],{},"An empty list means the selector matches nothing, or the pods are failing readiness.\nThat is the actual answer, and ping was never going to give it to you.",[477,4554,4555],{},"html pre.shiki code .siJwO, html code.shiki .siJwO{--shiki-default:#F69D50;--shiki-dark:#F69D50}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":47,"depth":47,"links":4557},[],"Why a failed ping proves nothing about whether your Service works.",{},"\u002Flearn\u002Fen\u002Farticles\u002F2026-08-06",{"title":4489,"description":4558},"learn\u002Fen\u002Farticles\u002F2026-08-06",[183],[223,4565],"debugging","g_WmoqBs1YOeMw7NsHp6F2JNMU1_QOW2iN0erDDOrcU",{"id":4568,"title":4569,"body":4570,"date":160,"description":4705,"extension":51,"lang":53,"meta":4706,"minutes":203,"navigation":24,"path":4707,"seo":4708,"stem":4709,"subjects":4710,"tags":4711,"__hash__":4714},"articles\u002Flearn\u002Fbn\u002Farticles\u002F2026-08-05.md","CSS-এ `gap` কেন `margin`-এর চেয়ে ভালো",{"type":36,"value":4571,"toc":4703},[4572,4586,4641,4651,4692,4695,4700],[39,4573,4574,4575,459,4578,4581,4582,4585],{},"আইটেমের মাঝে ফাঁক দিতে অনেকে এখনো ",[247,4576,4577],{},"margin-right",[247,4579,4580],{},"margin-bottom"," ব্যবহার\nকরেন। সমস্যা হলো শেষ আইটেমেও মার্জিন বসে যায়, তাই ",[247,4583,4584],{},":last-child"," দিয়ে সেটা\nআবার মুছতে হয়:",[241,4587,4591],{"className":4588,"code":4589,"language":4590,"meta":46,"style":46},"language-css shiki shiki-themes github-dark-dimmed github-dark-dimmed",".item {\n  margin-right: 16px;\n}\n.item:last-child {\n  margin-right: 0;\n}\n","css",[247,4592,4593,4600,4616,4620,4627,4637],{"__ignoreMap":46},[250,4594,4595,4598],{"class":252,"line":56},[250,4596,4597],{"class":389},".item",[250,4599,1064],{"class":263},[250,4601,4602,4605,4607,4610,4613],{"class":252,"line":203},[250,4603,4604],{"class":389},"  margin-right",[250,4606,1177],{"class":263},[250,4608,4609],{"class":389},"16",[250,4611,4612],{"class":255},"px",[250,4614,4615],{"class":263},";\n",[250,4617,4618],{"class":252,"line":47},[250,4619,287],{"class":263},[250,4621,4622,4625],{"class":252,"line":290},[250,4623,4624],{"class":389},".item:last-child",[250,4626,1064],{"class":263},[250,4628,4629,4631,4633,4635],{"class":252,"line":296},[250,4630,4604],{"class":389},[250,4632,1177],{"class":263},[250,4634,1006],{"class":389},[250,4636,4615],{"class":263},[250,4638,4639],{"class":252,"line":306},[250,4640,287],{"class":263},[39,4642,4643,4646,4647,4650],{},[247,4644,4645],{},"gap"," শুধু আইটেমগুলোর ",[947,4648,4649],{},"মাঝে"," ফাঁক দেয়, বাইরে নয়:",[241,4652,4654],{"className":4588,"code":4653,"language":4590,"meta":46,"style":46},".list {\n  display: flex;\n  gap: 16px;\n}\n",[247,4655,4656,4663,4675,4688],{"__ignoreMap":46},[250,4657,4658,4661],{"class":252,"line":56},[250,4659,4660],{"class":389},".list",[250,4662,1064],{"class":263},[250,4664,4665,4668,4670,4673],{"class":252,"line":203},[250,4666,4667],{"class":389},"  display",[250,4669,1177],{"class":263},[250,4671,4672],{"class":389},"flex",[250,4674,4615],{"class":263},[250,4676,4677,4680,4682,4684,4686],{"class":252,"line":47},[250,4678,4679],{"class":389},"  gap",[250,4681,1177],{"class":263},[250,4683,4609],{"class":389},[250,4685,4612],{"class":255},[250,4687,4615],{"class":263},[250,4689,4690],{"class":252,"line":290},[250,4691,287],{"class":263},[39,4693,4694],{},"দুই লাইন কম, আর র‍্যাপ হলেও ঠিক থাকে — মার্জিন দিয়ে করলে নতুন লাইনের\nউল্লম্ব ফাঁকটা আলাদা করে সামলাতে হতো।",[39,4696,4697,4699],{},[247,4698,4645],{}," এখন flexbox, grid আর multi-column — তিন জায়গাতেই কাজ করে।",[477,4701,4702],{},"html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":47,"depth":47,"links":4704},[],"শেষ আইটেমের বাড়তি ফাঁকা জায়গার সমস্যাটা আর থাকে না।",{},"\u002Flearn\u002Fbn\u002Farticles\u002F2026-08-05",{"title":4569,"description":4705},"learn\u002Fbn\u002Farticles\u002F2026-08-05",[3846],[4712,4713],"সিএসএস","লেআউট","dxjMm9zCcPNFphuaKEm0pRgCp8HLQDznYX1cULpnEbY",{"id":4716,"title":4717,"body":4718,"date":160,"description":4820,"extension":51,"lang":110,"meta":4821,"minutes":203,"navigation":24,"path":4822,"seo":4823,"stem":4824,"subjects":4825,"tags":4826,"__hash__":4828},"articles\u002Flearn\u002Fen\u002Farticles\u002F2026-08-05.md","await null still yields",{"type":36,"value":4719,"toc":4818},[4720,4726,4802,4809,4815],[39,4721,4722,4723,4725],{},"A common assumption is that ",[247,4724,1113],{}," on a non-promise is a no-op. It is not.",[241,4727,4729],{"className":243,"code":4728,"language":245,"meta":46,"style":46},"async function run() {\n  console.log('a')\n  await null\n  console.log('c')\n}\n\nrun()\nconsole.log('b')\n\u002F\u002F a, b, c\n",[247,4730,4731,4741,4753,4760,4772,4776,4780,4786,4798],{"__ignoreMap":46},[250,4732,4733,4735,4737,4739],{"class":252,"line":56},[250,4734,156],{"class":255},[250,4736,2530],{"class":255},[250,4738,2533],{"class":259},[250,4740,264],{"class":263},[250,4742,4743,4745,4747,4749,4751],{"class":252,"line":203},[250,4744,269],{"class":263},[250,4746,272],{"class":259},[250,4748,275],{"class":263},[250,4750,2546],{"class":278},[250,4752,282],{"class":263},[250,4754,4755,4757],{"class":252,"line":47},[250,4756,2553],{"class":255},[250,4758,4759],{"class":389}," null\n",[250,4761,4762,4764,4766,4768,4770],{"class":252,"line":290},[250,4763,269],{"class":263},[250,4765,272],{"class":259},[250,4767,275],{"class":263},[250,4769,2570],{"class":278},[250,4771,282],{"class":263},[250,4773,4774],{"class":252,"line":296},[250,4775,287],{"class":263},[250,4777,4778],{"class":252,"line":306},[250,4779,293],{"emptyLinePlaceholder":24},[250,4781,4782,4784],{"class":252,"line":315},[250,4783,2585],{"class":259},[250,4785,312],{"class":263},[250,4787,4788,4790,4792,4794,4796],{"class":252,"line":329},[250,4789,971],{"class":263},[250,4791,272],{"class":259},[250,4793,275],{"class":263},[250,4795,2598],{"class":278},[250,4797,282],{"class":263},[250,4799,4800],{"class":252,"line":334},[250,4801,2605],{"class":434},[39,4803,4804,4806,4807,1886],{},[247,4805,1113],{}," always suspends. The operand is wrapped, and everything after it is\nscheduled as a microtask — whether you awaited a promise, a number, or ",[247,4808,4466],{},[39,4810,4811,4812,4814],{},"The practical consequence: adding an ",[247,4813,1113],{}," to a function changes the ordering of\ncode that never touched promises. That is usually harmless and occasionally the\nexplanation for a bug that appeared when someone made a function async.",[477,4816,4817],{},"html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":47,"depth":47,"links":4819},[],"Awaiting a non-promise does not resolve synchronously.",{},"\u002Flearn\u002Fen\u002Farticles\u002F2026-08-05",{"title":4717,"description":4820},"learn\u002Fen\u002Farticles\u002F2026-08-05",[64],[156,4827],"event-loop","OR8sLwlVFtPv0ENwClhFGDSsY0bYxUet6wjNC7MeTJw",{"id":4830,"title":4831,"body":4832,"date":190,"description":4853,"extension":51,"lang":53,"meta":4854,"minutes":47,"navigation":24,"path":4855,"seo":4856,"stem":4857,"subjects":4858,"tags":4859,"__hash__":4861},"articles\u002Flearn\u002Fbn\u002Farticles\u002F2026-08-04.md","প্রথম ক্লায়েন্টের সাথে দাম নিয়ে কথা",{"type":36,"value":4833,"toc":4851},[4834,4837,4840,4843,4848],[39,4835,4836],{},"নতুনরা প্রায়ই ক্লায়েন্টকে আগে দাম বলতে দেন — ভাবেন, কম বলে ফেললে ক্ষতি।",[39,4838,4839],{},"বাস্তবে উল্টোটা ঘটে। যে সংখ্যাটা প্রথমে বলা হয়, পুরো আলোচনা তার আশেপাশেই\nঘোরে। ক্লায়েন্ট কম বললে আপনাকে সেখান থেকে উপরে ওঠার চেষ্টা করতে হয়, যেটা\nঅনেক কঠিন।",[39,4841,4842],{},"তার চেয়ে ভালো: কাজের পরিধি স্পষ্ট করে নিয়ে আপনি সংখ্যাটা বলুন। আর একটা\nসংখ্যা নয়, একটা পরিসর — সাথে কী কী অন্তর্ভুক্ত সেটাও।",[1761,4844,4845],{},[39,4846,4847],{},"\"এই স্কোপে ৮০০–১০০০ ডলার। এর মধ্যে ডিজাইন, ডেভেলপমেন্ট আর দুই দফা\nরিভিশন আছে। এর বাইরের পরিবর্তন ঘণ্টার হিসাবে।\"",[39,4849,4850],{},"শেষ বাক্যটাই সবচেয়ে গুরুত্বপূর্ণ। স্কোপ ক্রিপ — মানে ধাপে ধাপে কাজ বেড়ে\nযাওয়া — নতুন ফ্রিল্যান্সারদের সবচেয়ে বড় ক্ষতি করে, আর সেটা দাম কম বলার\nচেয়েও বেশি।",{"title":46,"searchDepth":47,"depth":47,"links":4852},[],"সংখ্যাটা আগে বলা প্রায় সবসময়ই ভালো।",{},"\u002Flearn\u002Fbn\u002Farticles\u002F2026-08-04",{"title":4831,"description":4853},"learn\u002Fbn\u002Farticles\u002F2026-08-04",[92],[95,4860],"দর","BU3Q0nQj7jfmzzI4flBILnWnj-r7Kr10oGd-ou7c2Po",{"id":4863,"title":4864,"body":4865,"date":190,"description":4917,"extension":51,"lang":110,"meta":4918,"minutes":47,"navigation":24,"path":4919,"seo":4920,"stem":4921,"subjects":4922,"tags":4923,"__hash__":4925},"articles\u002Flearn\u002Fen\u002Farticles\u002F2026-08-04.md","Jitter every TTL",{"type":36,"value":4866,"toc":4915},[4867,4870,4873,4876,4909,4912],[39,4868,4869],{},"A warm-up script writes ten thousand cache keys in one second, each with a\none-hour TTL.",[39,4871,4872],{},"One hour later, all ten thousand expire in the same second. Every request misses.\nThe database, sized for cached traffic, takes the full uncached load at once.",[39,4874,4875],{},"The incident happens an hour after a deploy that looked completely clean, which is\nwhat makes it hard to attribute.",[241,4877,4879],{"className":591,"code":4878,"language":593,"meta":46,"style":46},"ttl = base_ttl + random.randint(0, base_ttl \u002F\u002F 10)\n",[247,4880,4881],{"__ignoreMap":46},[250,4882,4883,4886,4888,4891,4893,4896,4898,4901,4904,4907],{"class":252,"line":56},[250,4884,4885],{"class":263},"ttl ",[250,4887,614],{"class":255},[250,4889,4890],{"class":263}," base_ttl ",[250,4892,2160],{"class":255},[250,4894,4895],{"class":263}," random.randint(",[250,4897,1006],{"class":389},[250,4899,4900],{"class":263},", base_ttl ",[250,4902,4903],{"class":255},"\u002F\u002F",[250,4905,4906],{"class":389}," 10",[250,4908,282],{"class":263},[39,4910,4911],{},"Ten percent of randomness spreads the expiry over six minutes instead of one\nsecond. It is one line, and it is the cheapest reliability improvement available to\nanyone running a cache.",[477,4913,4914],{},"html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":47,"depth":47,"links":4916},[],"Keys written together expire together, and stampede together.",{},"\u002Flearn\u002Fen\u002Farticles\u002F2026-08-04",{"title":4864,"description":4917},"learn\u002Fen\u002Farticles\u002F2026-08-04",[124],[127,4924],"reliability","HEWaBarpKAjHeTZV4CP4uOmXMopR-VWoxGO0m6LAclI",{"id":4927,"title":4928,"body":4929,"date":4945,"description":4946,"extension":51,"lang":110,"meta":4947,"minutes":203,"navigation":24,"path":4948,"seo":4949,"stem":4950,"subjects":4951,"tags":4953,"__hash__":4956},"articles\u002Flearn\u002Fen\u002Farticles\u002F2026-08-03.md","Disabled buttons are a dead end",{"type":36,"value":4930,"toc":4943},[4931,4934,4937,4940],[39,4932,4933],{},"A disabled submit button communicates that something is wrong and refuses to say\nwhat. It is also skipped by keyboard focus and often unreadable against its\nbackground, so a screen reader user gets no signal at all.",[39,4935,4936],{},"Keep the button enabled. Validate on submit. Move focus to the first error and\ndescribe it in text.",[39,4938,4939],{},"The user learns what the form wants by asking, which is the interaction they were\nalready attempting. The disabled state asks them to guess instead.",[39,4941,4942],{},"If the action is genuinely unavailable — a plan limit, a missing permission — say so\nin text next to the control. The reason is the useful part; the disabled state is\njust the absence of one.",{"title":46,"searchDepth":47,"depth":47,"links":4944},[],"2026-08-03","They remove the affordance without explaining the requirement.",{},"\u002Flearn\u002Fen\u002Farticles\u002F2026-08-03",{"title":4928,"description":4946},"learn\u002Fen\u002Farticles\u002F2026-08-03",[4952],"ui-ux",[4954,4955],"forms","accessibility","WM62nfpNMW97kvhd-JlTOgqauLUr4OkbyY5TalAHIS0",[4958,5904,6586,7170,7856,8023,8185,8694,8910,9033],{"id":4959,"title":4960,"author":4961,"body":4964,"category":3948,"cover":5892,"coverAlt":4960,"date":5893,"description":5894,"draft":6,"extension":51,"featured":24,"lang":53,"meta":5895,"navigation":24,"path":5896,"readingTime":334,"seo":5897,"stem":5898,"tags":5899,"__hash__":5903},"blog\u002Fblog\u002Fbn\u002Fbangla-typography-on-the-web.md","ওয়েবে বাংলা টাইপোগ্রাফি — যে সমস্যাগুলো নিয়ে কেউ লেখে না",{"name":4962,"role":4963},"তাসনিম আরা","প্রোডাক্ট ডিজাইনার",{"type":36,"value":4965,"toc":5882},[4966,4969,4972,4976,4979,5017,5024,5138,5145,5150,5153,5196,5199,5202,5262,5265,5269,5272,5279,5330,5337,5344,5347,5445,5452,5456,5463,5474,5544,5547,5551,5554,5591,5598,5602,5605,5608,5642,5645,5652,5825,5828,5832,5839,5868,5879],[39,4967,4968],{},"একটা ডিজাইন সিস্টেম ল্যাটিন স্ক্রিপ্টের জন্য বানানো হলে সেটা বাংলায় ভাঙে — কিন্তু এমনভাবে ভাঙে যে প্রথম দেখায় বোঝা যায় না। পেজ লোড হয়, লেখা পড়া যায়, কিছু একটা \"ঠিক লাগছে না\" — এইটুকুই।",[39,4970,4971],{},"এই সাইটে বাংলা ট্র্যাক যোগ করতে গিয়ে যে পাঁচটা জিনিস আলাদা করে ঠিক করতে হয়েছিল, সেগুলোই এখানে। প্রতিটার কারণ স্ক্রিপ্টের গঠনে, রুচিতে নয়।",[367,4973,4975],{"id":4974},"১-ফন্টে-গ্লিফই-নেই","১. ফন্টে গ্লিফই নেই",[39,4977,4978],{},"সবচেয়ে বড় সমস্যাটা সবচেয়ে সহজ। Inter, Geist, Satoshi — এদের কারও মধ্যে বাংলা গ্লিফ নেই। বাংলা লেখা এই ফন্টে রেন্ডার করতে দিলে ব্রাউজার fallback খোঁজে, আর সেটা যা পায় তা-ই দেখায়: সিস্টেম ফন্ট, বা টোফু বক্স।",[241,4980,4982],{"className":4588,"code":4981,"language":4590,"meta":46,"style":46},"\u002F* যথেষ্ট নয় *\u002F\nbody {\n  font-family: 'Inter', sans-serif;\n}\n",[247,4983,4984,4989,4996,5013],{"__ignoreMap":46},[250,4985,4986],{"class":252,"line":56},[250,4987,4988],{"class":434},"\u002F* যথেষ্ট নয় *\u002F\n",[250,4990,4991,4994],{"class":252,"line":203},[250,4992,4993],{"class":1173},"body",[250,4995,1064],{"class":263},[250,4997,4998,5001,5003,5006,5008,5011],{"class":252,"line":47},[250,4999,5000],{"class":389},"  font-family",[250,5002,1177],{"class":263},[250,5004,5005],{"class":278},"'Inter'",[250,5007,1687],{"class":263},[250,5009,5010],{"class":389},"sans-serif",[250,5012,4615],{"class":263},[250,5014,5015],{"class":252,"line":290},[250,5016,287],{"class":263},[39,5018,5019,5020,5023],{},"বাংলার জন্য আলাদা ফন্ট লাগবেই, আর সেটা ",[247,5021,5022],{},"subset"," ধরে আনতে হবে:",[241,5025,5029],{"className":5026,"code":5027,"language":5028,"meta":46,"style":46},"language-ts shiki shiki-themes github-dark-dimmed github-dark-dimmed","fonts: {\n  defaults: { subsets: ['latin'] },\n  families: [\n    { name: 'Inter', provider: 'google' },\n    {\n      name: 'Noto Sans Bengali',\n      provider: 'google',\n      subsets: ['bengali'], \u002F\u002F গ্লোবাল ডিফল্ট latin — এখানে override না করলে\n    },                       \u002F\u002F যে ফাইলটা আসবে তাতে একটাও দরকারি গ্লিফ নেই\n  ],\n}\n","ts",[247,5030,5031,5039,5058,5066,5082,5087,5098,5107,5121,5129,5134],{"__ignoreMap":46},[250,5032,5033,5036],{"class":252,"line":56},[250,5034,5035],{"class":709},"fonts",[250,5037,5038],{"class":263},": {\n",[250,5040,5041,5044,5047,5050,5052,5055],{"class":252,"line":203},[250,5042,5043],{"class":709},"  defaults",[250,5045,5046],{"class":263},": { ",[250,5048,5049],{"class":709},"subsets",[250,5051,1681],{"class":263},[250,5053,5054],{"class":278},"'latin'",[250,5056,5057],{"class":263},"] },\n",[250,5059,5060,5063],{"class":252,"line":47},[250,5061,5062],{"class":709},"  families",[250,5064,5065],{"class":263},": [\n",[250,5067,5068,5071,5073,5076,5079],{"class":252,"line":290},[250,5069,5070],{"class":263},"    { name: ",[250,5072,5005],{"class":278},[250,5074,5075],{"class":263},", provider: ",[250,5077,5078],{"class":278},"'google'",[250,5080,5081],{"class":263}," },\n",[250,5083,5084],{"class":252,"line":296},[250,5085,5086],{"class":263},"    {\n",[250,5088,5089,5092,5095],{"class":252,"line":306},[250,5090,5091],{"class":263},"      name: ",[250,5093,5094],{"class":278},"'Noto Sans Bengali'",[250,5096,5097],{"class":263},",\n",[250,5099,5100,5103,5105],{"class":252,"line":315},[250,5101,5102],{"class":263},"      provider: ",[250,5104,5078],{"class":278},[250,5106,5097],{"class":263},[250,5108,5109,5112,5115,5118],{"class":252,"line":329},[250,5110,5111],{"class":263},"      subsets: [",[250,5113,5114],{"class":278},"'bengali'",[250,5116,5117],{"class":263},"], ",[250,5119,5120],{"class":434},"\u002F\u002F গ্লোবাল ডিফল্ট latin — এখানে override না করলে\n",[250,5122,5123,5126],{"class":252,"line":334},[250,5124,5125],{"class":263},"    },                       ",[250,5127,5128],{"class":434},"\u002F\u002F যে ফাইলটা আসবে তাতে একটাও দরকারি গ্লিফ নেই\n",[250,5130,5131],{"class":252,"line":339},[250,5132,5133],{"class":263},"  ],\n",[250,5135,5136],{"class":252,"line":864},[250,5137,287],{"class":263},[39,5139,5140,5141,5144],{},"শেষ লাইনটা আসল ফাঁদ। গ্লোবাল ",[247,5142,5143],{},"subsets: ['latin']"," থাকলে Noto Sans Bengali-ও latin subset নিয়ে আসবে — ফাইলটা ডাউনলোড হবে, ওজন যোগ হবে, আর একটা বাংলা অক্ষরও দেখাবে না।",[5146,5147,5149],"h3",{"id":5148},"fallback-এর-ক্রমটাও-ভাবার-বিষয়","fallback-এর ক্রমটাও ভাবার বিষয়",[39,5151,5152],{},"বাংলা পেজে ইংরেজিও থাকে — কোড, প্রোডাক্টের নাম, সংখ্যা। ব্রাউজার প্রতি-গ্লিফ ধরে fallback করে, তাই ক্রম ঠিক করে দিলে দুটোই ঠিক ফন্ট পায়:",[241,5154,5156],{"className":4588,"code":5155,"language":4590,"meta":46,"style":46},":lang(bn) {\n  font-family: 'Noto Sans Bengali', 'Inter', ui-sans-serif, sans-serif;\n}\n",[247,5157,5158,5169,5192],{"__ignoreMap":46},[250,5159,5160,5163,5165,5167],{"class":252,"line":56},[250,5161,5162],{"class":389},":lang",[250,5164,275],{"class":263},[250,5166,53],{"class":389},[250,5168,429],{"class":263},[250,5170,5171,5173,5175,5177,5179,5181,5183,5186,5188,5190],{"class":252,"line":203},[250,5172,5000],{"class":389},[250,5174,1177],{"class":263},[250,5176,5094],{"class":278},[250,5178,1687],{"class":263},[250,5180,5005],{"class":278},[250,5182,1687],{"class":263},[250,5184,5185],{"class":389},"ui-sans-serif",[250,5187,1687],{"class":263},[250,5189,5010],{"class":389},[250,5191,4615],{"class":263},[250,5193,5194],{"class":252,"line":47},[250,5195,287],{"class":263},[39,5197,5198],{},"Noto আগে, তাই বাংলা Noto পায়। ইংরেজি অক্ষর Noto-র ল্যাটিন সেটে আছে বটে, কিন্তু সেটা দেখতে অন্যরকম — পাতায় আলাদা রঙের মতো লাগে। Inter দ্বিতীয়তে থাকায় ল্যাটিন গ্লিফগুলো Inter-এই পড়ে।",[39,5200,5201],{},"কোড কিন্তু দুই ট্র্যাকেই ল্যাটিন থাকা উচিত:",[241,5203,5205],{"className":4588,"code":5204,"language":4590,"meta":46,"style":46},":lang(bn) :is(code, pre, kbd, samp) {\n  font-family: var(--font-mono);\n}\n",[247,5206,5207,5241,5258],{"__ignoreMap":46},[250,5208,5209,5211,5213,5215,5218,5221,5223,5225,5227,5229,5231,5234,5236,5239],{"class":252,"line":56},[250,5210,5162],{"class":389},[250,5212,275],{"class":263},[250,5214,53],{"class":389},[250,5216,5217],{"class":263},") ",[250,5219,5220],{"class":389},":is",[250,5222,275],{"class":263},[250,5224,247],{"class":1173},[250,5226,1687],{"class":263},[250,5228,241],{"class":1173},[250,5230,1687],{"class":263},[250,5232,5233],{"class":1173},"kbd",[250,5235,1687],{"class":263},[250,5237,5238],{"class":1173},"samp",[250,5240,429],{"class":263},[250,5242,5243,5245,5247,5250,5252,5255],{"class":252,"line":203},[250,5244,5000],{"class":389},[250,5246,1177],{"class":263},[250,5248,5249],{"class":389},"var",[250,5251,275],{"class":263},[250,5253,5254],{"class":709},"--font-mono",[250,5256,5257],{"class":263},");\n",[250,5259,5260],{"class":252,"line":47},[250,5261,287],{"class":263},[39,5263,5264],{},"একটা shell কমান্ড বাংলা ফন্টে রেন্ডার করার কোনো কারণ নেই, আর কোড ব্লকের লেআউট মনোস্পেসের গ্লিফ-প্রস্থ ধরে বানানো।",[367,5266,5268],{"id":5267},"২-মাত্রা-উপরে-নিচে-জায়গা-চায়","২. মাত্রা উপরে-নিচে জায়গা চায়",[39,5270,5271],{},"এইটা সবচেয়ে কম আলোচিত, আর সবচেয়ে বেশি চোখে লাগে।",[39,5273,5274,5275,5278],{},"বাংলায় কার-চিহ্ন বেসলাইনের উপরে আর নিচে বসে — ি, ী, ু, ূ, ৃ, ে, ৈ, ো, ৌ, রেফ, হসন্ত, যুক্তাক্ষরের নিচের অংশ। ল্যাটিন প্রোজে যে ",[247,5276,5277],{},"line-height: 1.6"," স্বাভাবিক, বাংলায় সেই মাপে পরপর দুই লাইনের চিহ্নগুলো একে অপরকে ছুঁয়ে ফেলে।",[241,5280,5282],{"className":4588,"code":5281,"language":4590,"meta":46,"style":46},":lang(bn) .ux-prose :is(p, li, blockquote) {\n  line-height: 1.9;\n}\n",[247,5283,5284,5314,5326],{"__ignoreMap":46},[250,5285,5286,5288,5290,5292,5294,5297,5300,5302,5304,5306,5308,5310,5312],{"class":252,"line":56},[250,5287,5162],{"class":389},[250,5289,275],{"class":263},[250,5291,53],{"class":389},[250,5293,5217],{"class":263},[250,5295,5296],{"class":389},".ux-prose",[250,5298,5299],{"class":389}," :is",[250,5301,275],{"class":263},[250,5303,39],{"class":1173},[250,5305,1687],{"class":263},[250,5307,514],{"class":1173},[250,5309,1687],{"class":263},[250,5311,1761],{"class":1173},[250,5313,429],{"class":263},[250,5315,5316,5319,5321,5324],{"class":252,"line":203},[250,5317,5318],{"class":389},"  line-height",[250,5320,1177],{"class":263},[250,5322,5323],{"class":389},"1.9",[250,5325,4615],{"class":263},[250,5327,5328],{"class":252,"line":47},[250,5329,287],{"class":263},[39,5331,5332,5333,5336],{},"কিন্তু ",[517,5334,5335],{},"সব সাইজে এক মান বসানো ভুল"," — আর এই ভুলটা আমরা করেছিলাম।",[39,5338,5339,5340,5343],{},"প্রথমে লেখা ছিল ",[247,5341,5342],{},":lang(bn) :is(h1, h2, h3) { line-height: 1.45 }","। বডি টেক্সটের মাপে এটা ঠিক। কিন্তু পেজ টাইটেল ক্ল্যাম্প হয়ে ৬৪px-এ যায় — সেখানে ১.৪৫ মানে দুই লাইনের মাঝে ৯৩px ফাঁক। দুই শব্দের একটা হেডিং দেখাচ্ছিল যেন ডাবল-স্পেসড।",[39,5345,5346],{},"টাইপ যত বড়, আনুপাতিক লিডিং তত কম দরকার। একটা fixed multiplier ঠিক উল্টোটা করে। তাই প্রতিটা ধাপে আলাদা:",[241,5348,5350],{"className":4588,"code":5349,"language":4590,"meta":46,"style":46},":lang(bn) {\n  --text-hero--line-height: 1.16; \u002F* ল্যাটিনের চেয়ে সামান্যই বেশি *\u002F\n  --text-section--line-height: 1.22;\n  --text-title--line-height: 1.35;\n  --text-lg--line-height: 1.75;\n  --text-base--line-height: 1.8; \u002F* পড়ার মাপে উদার *\u002F\n  --text-sm--line-height: 1.65;\n}\n",[247,5351,5352,5362,5378,5390,5402,5414,5429,5441],{"__ignoreMap":46},[250,5353,5354,5356,5358,5360],{"class":252,"line":56},[250,5355,5162],{"class":389},[250,5357,275],{"class":263},[250,5359,53],{"class":389},[250,5361,429],{"class":263},[250,5363,5364,5367,5369,5372,5375],{"class":252,"line":203},[250,5365,5366],{"class":709},"  --text-hero--line-height",[250,5368,1177],{"class":263},[250,5370,5371],{"class":389},"1.16",[250,5373,5374],{"class":263},"; ",[250,5376,5377],{"class":434},"\u002F* ল্যাটিনের চেয়ে সামান্যই বেশি *\u002F\n",[250,5379,5380,5383,5385,5388],{"class":252,"line":47},[250,5381,5382],{"class":709},"  --text-section--line-height",[250,5384,1177],{"class":263},[250,5386,5387],{"class":389},"1.22",[250,5389,4615],{"class":263},[250,5391,5392,5395,5397,5400],{"class":252,"line":290},[250,5393,5394],{"class":709},"  --text-title--line-height",[250,5396,1177],{"class":263},[250,5398,5399],{"class":389},"1.35",[250,5401,4615],{"class":263},[250,5403,5404,5407,5409,5412],{"class":252,"line":296},[250,5405,5406],{"class":709},"  --text-lg--line-height",[250,5408,1177],{"class":263},[250,5410,5411],{"class":389},"1.75",[250,5413,4615],{"class":263},[250,5415,5416,5419,5421,5424,5426],{"class":252,"line":306},[250,5417,5418],{"class":709},"  --text-base--line-height",[250,5420,1177],{"class":263},[250,5422,5423],{"class":389},"1.8",[250,5425,5374],{"class":263},[250,5427,5428],{"class":434},"\u002F* পড়ার মাপে উদার *\u002F\n",[250,5430,5431,5434,5436,5439],{"class":252,"line":315},[250,5432,5433],{"class":709},"  --text-sm--line-height",[250,5435,1177],{"class":263},[250,5437,5438],{"class":389},"1.65",[250,5440,4615],{"class":263},[250,5442,5443],{"class":252,"line":329},[250,5444,287],{"class":263},[39,5446,5447,5448,5451],{},"টোকেনটা রিডিফাইন করাই যথেষ্ট, কারণ ইউটিলিটিগুলো ",[247,5449,5450],{},"line-height: var(--text-\u003Csize>--line-height)","-এ কম্পাইল হয়। কোনো সিলেক্টর লিখতে হয় না, আর ওই ধাপ ব্যবহার করা প্রতিটা এলিমেন্ট আপনাআপনি ঠিক হয়ে যায়।",[367,5453,5455],{"id":5454},"৩-uppercase-বাংলায়-একটা-no-op","৩. uppercase বাংলায় একটা no-op",[39,5457,5458,5459,5462],{},"বাংলায় বড় হাতের অক্ষর নেই। ",[247,5460,5461],{},"text-transform: uppercase"," কিছুই বদলায় না।",[39,5464,5465,5466,5469,5470,5473],{},"সমস্যা হলো, eyebrow বা badge-এর স্টাইলে ",[247,5467,5468],{},"uppercase"," একা আসে না — সাথে ",[247,5471,5472],{},"letter-spacing"," আসে। ল্যাটিনে বড় হাতের অক্ষর ফাঁক ফাঁক করে বসালে ভালো লাগে। বাংলায় ট্র্যাকিং বাড়ালে যুক্তাক্ষর টেনে আলাদা হয়ে যায়, আর লেবেলটা ভাঙা দেখায়।",[241,5475,5477],{"className":4588,"code":5476,"language":4590,"meta":46,"style":46},":lang(bn) :is(.ux-eyebrow, .uppercase, [class*='tracking-']) {\n  text-transform: none;\n  letter-spacing: normal;\n}\n",[247,5478,5479,5516,5528,5540],{"__ignoreMap":46},[250,5480,5481,5483,5485,5487,5489,5491,5493,5496,5498,5501,5504,5507,5510,5513],{"class":252,"line":56},[250,5482,5162],{"class":389},[250,5484,275],{"class":263},[250,5486,53],{"class":389},[250,5488,5217],{"class":263},[250,5490,5220],{"class":389},[250,5492,275],{"class":263},[250,5494,5495],{"class":389},".ux-eyebrow",[250,5497,1687],{"class":263},[250,5499,5500],{"class":389},".uppercase",[250,5502,5503],{"class":263},", [",[250,5505,5506],{"class":389},"class",[250,5508,5509],{"class":255},"*=",[250,5511,5512],{"class":278},"'tracking-'",[250,5514,5515],{"class":263},"]) {\n",[250,5517,5518,5521,5523,5526],{"class":252,"line":203},[250,5519,5520],{"class":389},"  text-transform",[250,5522,1177],{"class":263},[250,5524,5525],{"class":389},"none",[250,5527,4615],{"class":263},[250,5529,5530,5533,5535,5538],{"class":252,"line":47},[250,5531,5532],{"class":389},"  letter-spacing",[250,5534,1177],{"class":263},[250,5536,5537],{"class":389},"normal",[250,5539,4615],{"class":263},[250,5541,5542],{"class":252,"line":290},[250,5543,287],{"class":263},[39,5545,5546],{},"দুটোই বাতিল করা হয়েছে, নতুন করে স্টাইল করা হয়নি — ইংরেজি ট্র্যাক তার নিজের ট্রিটমেন্ট অক্ষত রাখে।",[367,5548,5550],{"id":5549},"৪-একই-px-এ-বাংলা-ছোট-দেখায়","৪. একই px-এ বাংলা ছোট দেখায়",[39,5552,5553],{},"একই ফন্ট-সাইজে বাংলার x-height ল্যাটিনের তুলনায় কম, তাই পাশাপাশি রাখলে বাংলাটা ছোট লাগে। খুব সামান্য একটা বাম্প দুই ট্র্যাককে একই ডিজাইন সিস্টেমের মতো দেখায়:",[241,5555,5557],{"className":4588,"code":5556,"language":4590,"meta":46,"style":46},":lang(bn) .ux-prose {\n  font-size: 1.03em;\n}\n",[247,5558,5559,5573,5587],{"__ignoreMap":46},[250,5560,5561,5563,5565,5567,5569,5571],{"class":252,"line":56},[250,5562,5162],{"class":389},[250,5564,275],{"class":263},[250,5566,53],{"class":389},[250,5568,5217],{"class":263},[250,5570,5296],{"class":389},[250,5572,1064],{"class":263},[250,5574,5575,5578,5580,5583,5585],{"class":252,"line":203},[250,5576,5577],{"class":389},"  font-size",[250,5579,1177],{"class":263},[250,5581,5582],{"class":389},"1.03",[250,5584,947],{"class":255},[250,5586,4615],{"class":263},[250,5588,5589],{"class":252,"line":47},[250,5590,287],{"class":263},[39,5592,5593,1687,5595,5597],{},[247,5594,947],{},[247,5596,4612],{}," নয় — যাতে ব্যবহারকারীর নিজের ফন্ট-সাইজ সেটিং কাজ করতে থাকে।",[367,5599,5601],{"id":5600},"৫-সংখ্যার-হাইড্রেশন-মিসম্যাচ","৫. সংখ্যার হাইড্রেশন মিসম্যাচ",[39,5603,5604],{},"এইটা টাইপোগ্রাফির সমস্যা নয়, কিন্তু একই কাজ করতে গিয়ে ধরা পড়ে।",[39,5606,5607],{},"বাংলা সংখ্যা ফরম্যাট করতে স্বাভাবিক পছন্দ:",[241,5609,5611],{"className":5026,"code":5610,"language":5028,"meta":46,"style":46},"new Intl.NumberFormat('bn-BD').format(12480)\n",[247,5612,5613],{"__ignoreMap":46},[250,5614,5615,5618,5621,5624,5626,5629,5632,5635,5637,5640],{"class":252,"line":56},[250,5616,5617],{"class":255},"new",[250,5619,5620],{"class":263}," Intl.",[250,5622,5623],{"class":259},"NumberFormat",[250,5625,275],{"class":263},[250,5627,5628],{"class":278},"'bn-BD'",[250,5630,5631],{"class":263},").",[250,5633,5634],{"class":259},"format",[250,5636,275],{"class":263},[250,5638,5639],{"class":389},"12480",[250,5641,282],{"class":263},[39,5643,5644],{},"কিন্তু ICU বিল্ডগুলো একমত নয় — কেউ হাজার ধরে গ্রুপ করে (১২,৪৮০), কেউ লাখ ধরে (১২,৪৮০ বনাম ১,২৪৮০ ধরনের পার্থক্য)। Node-এর ICU আর ব্রাউজারের ICU আলাদা হলে সার্ভার একরকম লেখে, ক্লায়েন্ট আরেকরকম — আর prerendered পেজে সেটা সরাসরি hydration mismatch।",[39,5646,5647,5648,5651],{},"তাই গ্রুপিংটা ",[247,5649,5650],{},"en-US"," থেকে নিয়ে শুধু অঙ্কগুলো বদলানো হয়:",[241,5653,5655],{"className":5026,"code":5654,"language":5028,"meta":46,"style":46},"const BN_DIGITS = ['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯']\n\nexport function formatBnNumber(value: number): string {\n  return new Intl.NumberFormat('en-US')\n    .format(value)\n    .replace(\u002F\\d\u002Fg, (d) => BN_DIGITS[Number(d)]!)\n}\n",[247,5656,5657,5715,5719,5749,5766,5776,5821],{"__ignoreMap":46},[250,5658,5659,5661,5664,5666,5669,5672,5674,5676,5678,5680,5682,5684,5686,5688,5690,5693,5695,5698,5700,5703,5705,5708,5710,5713],{"class":252,"line":56},[250,5660,386],{"class":255},[250,5662,5663],{"class":389}," BN_DIGITS",[250,5665,393],{"class":255},[250,5667,5668],{"class":263}," [",[250,5670,5671],{"class":278},"'০'",[250,5673,1687],{"class":263},[250,5675,1845],{"class":278},[250,5677,1687],{"class":263},[250,5679,1868],{"class":278},[250,5681,1687],{"class":263},[250,5683,1908],{"class":278},[250,5685,1687],{"class":263},[250,5687,1926],{"class":278},[250,5689,1687],{"class":263},[250,5691,5692],{"class":278},"'৫'",[250,5694,1687],{"class":263},[250,5696,5697],{"class":278},"'৬'",[250,5699,1687],{"class":263},[250,5701,5702],{"class":278},"'৭'",[250,5704,1687],{"class":263},[250,5706,5707],{"class":278},"'৮'",[250,5709,1687],{"class":263},[250,5711,5712],{"class":278},"'৯'",[250,5714,1698],{"class":263},[250,5716,5717],{"class":252,"line":203},[250,5718,293],{"emptyLinePlaceholder":24},[250,5720,5721,5724,5726,5729,5731,5734,5736,5739,5742,5744,5747],{"class":252,"line":47},[250,5722,5723],{"class":255},"export",[250,5725,2530],{"class":255},[250,5727,5728],{"class":259}," formatBnNumber",[250,5730,275],{"class":263},[250,5732,5733],{"class":709},"value",[250,5735,2274],{"class":255},[250,5737,5738],{"class":389}," number",[250,5740,5741],{"class":263},")",[250,5743,2274],{"class":255},[250,5745,5746],{"class":389}," string",[250,5748,1064],{"class":263},[250,5750,5751,5753,5755,5757,5759,5761,5764],{"class":252,"line":290},[250,5752,808],{"class":255},[250,5754,1072],{"class":255},[250,5756,5620],{"class":263},[250,5758,5623],{"class":259},[250,5760,275],{"class":263},[250,5762,5763],{"class":278},"'en-US'",[250,5765,282],{"class":263},[250,5767,5768,5771,5773],{"class":252,"line":296},[250,5769,5770],{"class":263},"    .",[250,5772,5634],{"class":259},[250,5774,5775],{"class":263},"(value)\n",[250,5777,5778,5780,5783,5785,5787,5790,5792,5795,5798,5801,5803,5805,5807,5810,5813,5816,5819],{"class":252,"line":306},[250,5779,5770],{"class":263},[250,5781,5782],{"class":259},"replace",[250,5784,275],{"class":263},[250,5786,1117],{"class":278},[250,5788,5789],{"class":389},"\\d",[250,5791,1117],{"class":278},[250,5793,5794],{"class":255},"g",[250,5796,5797],{"class":263},", (",[250,5799,5800],{"class":709},"d",[250,5802,5217],{"class":263},[250,5804,990],{"class":255},[250,5806,5663],{"class":389},[250,5808,5809],{"class":263},"[",[250,5811,5812],{"class":259},"Number",[250,5814,5815],{"class":263},"(d)]",[250,5817,5818],{"class":255},"!",[250,5820,282],{"class":263},[250,5822,5823],{"class":252,"line":315},[250,5824,287],{"class":263},[39,5826,5827],{},"দেখতে হ্যাক লাগে। কিন্তু এটা deterministic, আর দুই রানটাইমে এক আউটপুট দেয় — যেটাই আসল দাবি।",[367,5829,5831],{"id":5830},"সব-মিলিয়ে","সব মিলিয়ে",[39,5833,5834,5835,5838],{},"লক্ষ্য করার মতো ব্যাপার: এর কোনোটাই \"বাংলার জন্য আলাদা ডিজাইন\" নয়। সবগুলো ",[247,5836,5837],{},":lang(bn)","-এর ভেতরে, একটা আলাদা ফাইলে, সবার শেষে ইমপোর্ট করা — যাতে ল্যাটিন ডিফল্টের উপরে জেতে।",[241,5840,5842],{"className":4588,"code":5841,"language":4590,"meta":46,"style":46},"@import '.\u002Fprose.css';\n\u002F* সবার শেষে, যাতে :lang(bn) আগেরগুলোকে override করে *\u002F\n@import '.\u002Fbangla.css';\n",[247,5843,5844,5854,5859],{"__ignoreMap":46},[250,5845,5846,5849,5852],{"class":252,"line":56},[250,5847,5848],{"class":255},"@import",[250,5850,5851],{"class":278}," '.\u002Fprose.css'",[250,5853,4615],{"class":263},[250,5855,5856],{"class":252,"line":203},[250,5857,5858],{"class":434},"\u002F* সবার শেষে, যাতে :lang(bn) আগেরগুলোকে override করে *\u002F\n",[250,5860,5861,5863,5866],{"class":252,"line":47},[250,5862,5848],{"class":255},[250,5864,5865],{"class":278}," '.\u002Fbangla.css'",[250,5867,4615],{"class":263},[39,5869,5870,5871,5874,5875,5878],{},"ইংরেজি ট্র্যাক এই ফাইলটার একটা লাইনও ছোঁয় না। আর ",[247,5872,5873],{},"\u003Chtml lang>"," ভুল হলে পুরো স্টাইলশিটটা চুপচাপ নিষ্ক্রিয় হয়ে যায় — উপসর্গ দেখায় \"বাংলা ভুল ফন্টে রেন্ডার হচ্ছে\", যেটা আসল কারণ থেকে অনেক দূরে। তাই ",[247,5876,5877],{},"lang"," অ্যাট্রিবিউটটা রুট থেকে সেট হয়, প্রতি পেজে নয়, আর একটা নতুন রুট সেটা ভুলে যেতে পারে না।",[477,5880,5881],{},"html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}html pre.shiki code .sza-u, html code.shiki .sza-u{--shiki-default:#8DDB8C;--shiki-dark:#8DDB8C}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siJwO, html code.shiki .siJwO{--shiki-default:#F69D50;--shiki-dark:#F69D50}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}",{"title":46,"searchDepth":47,"depth":47,"links":5883},[5884,5887,5888,5889,5890,5891],{"id":4974,"depth":203,"text":4975,"children":5885},[5886],{"id":5148,"depth":47,"text":5149},{"id":5267,"depth":203,"text":5268},{"id":5454,"depth":203,"text":5455},{"id":5549,"depth":203,"text":5550},{"id":5600,"depth":203,"text":5601},{"id":5830,"depth":203,"text":5831},"\u002Fcovers\u002Fblog\u002Fbn\u002Fbangla-typography-on-the-web.webp","2026-07-22","লাইন-হাইট, মাত্রার সংঘর্ষ, uppercase-এর অর্থহীনতা আর ফন্ট ফলব্যাক। ল্যাটিনের জন্য বানানো একটা ডিজাইন সিস্টেমে বাংলা বসাতে গিয়ে যা যা ঠিক করতে হয়েছিল।",{},"\u002Fblog\u002Fbn\u002Fbangla-typography-on-the-web",{"title":4960,"description":5894},"blog\u002Fbn\u002Fbangla-typography-on-the-web",[5900,5901,5902],"টাইপোগ্রাফি","বাংলা","CSS","3-22xvnVCGspPollS842CYIXXRW-Q1sYELaRo6T-o3U",{"id":5905,"title":5906,"author":5907,"body":5910,"category":6574,"cover":6575,"coverAlt":5906,"date":4183,"description":6576,"draft":6,"extension":51,"featured":6,"lang":53,"meta":6577,"navigation":24,"path":6578,"readingTime":329,"seo":6579,"stem":6580,"tags":6581,"__hash__":6585},"blog\u002Fblog\u002Fbn\u002Fdesign-system-mistakes.md","ডিজাইন সিস্টেম বানানোর আগে যে ভুলগুলো এড়ানো দরকার",{"name":5908,"role":5909},"নাফিস রহমান","ডিজাইন সিস্টেম লিড",{"type":36,"value":5911,"toc":6565},[5912,5915,5919,5922,6039,6049,6055,6059,6065,6265,6276,6279,6325,6332,6336,6345,6357,6361,6364,6414,6428,6431,6435,6438,6441,6445,6448,6539,6546,6550,6556,6559,6562],[39,5913,5914],{},"একটা ডিজাইন সিস্টেম হয় তার প্রথম রিব্র্যান্ড পার করে, নয়তো পারে না। বেশিরভাগ পারে না — আর কারণটা প্রায় সবসময় একই: সিস্টেমে দুইটা লেয়ার আছে, যেখানে দরকার ছিল তিনটা।",[367,5916,5918],{"id":5917},"দুই-লেয়ারের-ফাঁদ","দুই-লেয়ারের ফাঁদ",[39,5920,5921],{},"প্রায় প্রতিটা টোকেন সিস্টেম এইভাবে শুরু হয়। একটা প্যালেট, আর কম্পোনেন্ট যেগুলো সেটা ব্যবহার করে।",[241,5923,5925],{"className":4588,"code":5924,"language":4590,"meta":46,"style":46},":root {\n  --blue-500: #00d4ff;\n  --gray-900: #18181b;\n}\n\n.card {\n  background: var(--gray-900);\n  border-color: var(--gray-800);\n}\n\n.button-primary {\n  background: var(--blue-500);\n}\n",[247,5926,5927,5934,5946,5958,5962,5966,5973,5989,6005,6009,6013,6020,6035],{"__ignoreMap":46},[250,5928,5929,5932],{"class":252,"line":56},[250,5930,5931],{"class":389},":root",[250,5933,1064],{"class":263},[250,5935,5936,5939,5941,5944],{"class":252,"line":203},[250,5937,5938],{"class":709},"  --blue-500",[250,5940,1177],{"class":263},[250,5942,5943],{"class":389},"#00d4ff",[250,5945,4615],{"class":263},[250,5947,5948,5951,5953,5956],{"class":252,"line":47},[250,5949,5950],{"class":709},"  --gray-900",[250,5952,1177],{"class":263},[250,5954,5955],{"class":389},"#18181b",[250,5957,4615],{"class":263},[250,5959,5960],{"class":252,"line":290},[250,5961,287],{"class":263},[250,5963,5964],{"class":252,"line":296},[250,5965,293],{"emptyLinePlaceholder":24},[250,5967,5968,5971],{"class":252,"line":306},[250,5969,5970],{"class":389},".card",[250,5972,1064],{"class":263},[250,5974,5975,5978,5980,5982,5984,5987],{"class":252,"line":315},[250,5976,5977],{"class":389},"  background",[250,5979,1177],{"class":263},[250,5981,5249],{"class":389},[250,5983,275],{"class":263},[250,5985,5986],{"class":709},"--gray-900",[250,5988,5257],{"class":263},[250,5990,5991,5994,5996,5998,6000,6003],{"class":252,"line":329},[250,5992,5993],{"class":389},"  border-color",[250,5995,1177],{"class":263},[250,5997,5249],{"class":389},[250,5999,275],{"class":263},[250,6001,6002],{"class":709},"--gray-800",[250,6004,5257],{"class":263},[250,6006,6007],{"class":252,"line":334},[250,6008,287],{"class":263},[250,6010,6011],{"class":252,"line":339},[250,6012,293],{"emptyLinePlaceholder":24},[250,6014,6015,6018],{"class":252,"line":864},[250,6016,6017],{"class":389},".button-primary",[250,6019,1064],{"class":263},[250,6021,6022,6024,6026,6028,6030,6033],{"class":252,"line":1590},[250,6023,5977],{"class":389},[250,6025,1177],{"class":263},[250,6027,5249],{"class":389},[250,6029,275],{"class":263},[250,6031,6032],{"class":709},"--blue-500",[250,6034,5257],{"class":263},[250,6036,6037],{"class":252,"line":1600},[250,6038,287],{"class":263},[39,6040,6041,6042,6044,6045,6048],{},"দেখতে পরিষ্কার। ছয় মাস ভালোই চলে। তারপর ব্র্যান্ড বদলায় — নীল থেকে সবুজ — আর তখন বোঝা যায় ",[247,6043,6032],{}," নামটাই সমস্যা। এখন হয় সবুজ রঙ একটা ",[247,6046,6047],{},"blue"," নামের ভেরিয়েবলে রাখতে হবে, নয়তো কোডবেসে ৪০০টা জায়গায় খুঁজে বদলাতে হবে।",[39,6050,6051,6052,6054],{},"আরও খারাপ ব্যাপারটা হলো ডার্ক মোড। ",[247,6053,5986],{}," ডার্ক থিমে ব্যাকগ্রাউন্ড, লাইট থিমে টেক্সট। একই টোকেন, দুইটা উল্টো ভূমিকা। কম্পোনেন্ট যখন সরাসরি প্যালেট ছোঁয়, তখন প্রতিটা কম্পোনেন্টকে জানতে হয় থিম কী — আর সেটাই সিস্টেমটার মৃত্যু।",[367,6056,6058],{"id":6057},"তৃতীয়-লেয়ারটা-ভূমিকা","তৃতীয় লেয়ারটা: ভূমিকা",[39,6060,6061,6062,1944],{},"মাঝখানে যা দরকার, সেটা রঙ নয় — ",[517,6063,6064],{},"ভূমিকা",[241,6066,6068],{"className":4588,"code":6067,"language":4590,"meta":46,"style":46},"\u002F* লেয়ার ১ — প্রিমিটিভ। কোনো প্রোডাক্ট-অর্থ নেই। *\u002F\n:root {\n  --cyan-400: #22d3ee;\n  --cyan-600: #0891b2;\n  --zinc-50: #fafafa;\n  --zinc-900: #18181b;\n}\n\n\u002F* লেয়ার ২ — সিমান্টিক। থিম অনুযায়ী বদলায়। *\u002F\n:root {\n  --surface: var(--zinc-50);\n  --foreground: var(--zinc-900);\n  --primary: var(--cyan-600);\n}\n\n[data-theme='dark'] {\n  --surface: var(--zinc-900);\n  --foreground: var(--zinc-50);\n  --primary: var(--cyan-400);\n}\n",[247,6069,6070,6075,6081,6093,6105,6117,6128,6132,6136,6141,6147,6163,6179,6195,6199,6203,6218,6232,6246,6261],{"__ignoreMap":46},[250,6071,6072],{"class":252,"line":56},[250,6073,6074],{"class":434},"\u002F* লেয়ার ১ — প্রিমিটিভ। কোনো প্রোডাক্ট-অর্থ নেই। *\u002F\n",[250,6076,6077,6079],{"class":252,"line":203},[250,6078,5931],{"class":389},[250,6080,1064],{"class":263},[250,6082,6083,6086,6088,6091],{"class":252,"line":47},[250,6084,6085],{"class":709},"  --cyan-400",[250,6087,1177],{"class":263},[250,6089,6090],{"class":389},"#22d3ee",[250,6092,4615],{"class":263},[250,6094,6095,6098,6100,6103],{"class":252,"line":290},[250,6096,6097],{"class":709},"  --cyan-600",[250,6099,1177],{"class":263},[250,6101,6102],{"class":389},"#0891b2",[250,6104,4615],{"class":263},[250,6106,6107,6110,6112,6115],{"class":252,"line":296},[250,6108,6109],{"class":709},"  --zinc-50",[250,6111,1177],{"class":263},[250,6113,6114],{"class":389},"#fafafa",[250,6116,4615],{"class":263},[250,6118,6119,6122,6124,6126],{"class":252,"line":306},[250,6120,6121],{"class":709},"  --zinc-900",[250,6123,1177],{"class":263},[250,6125,5955],{"class":389},[250,6127,4615],{"class":263},[250,6129,6130],{"class":252,"line":315},[250,6131,287],{"class":263},[250,6133,6134],{"class":252,"line":329},[250,6135,293],{"emptyLinePlaceholder":24},[250,6137,6138],{"class":252,"line":334},[250,6139,6140],{"class":434},"\u002F* লেয়ার ২ — সিমান্টিক। থিম অনুযায়ী বদলায়। *\u002F\n",[250,6142,6143,6145],{"class":252,"line":339},[250,6144,5931],{"class":389},[250,6146,1064],{"class":263},[250,6148,6149,6152,6154,6156,6158,6161],{"class":252,"line":864},[250,6150,6151],{"class":709},"  --surface",[250,6153,1177],{"class":263},[250,6155,5249],{"class":389},[250,6157,275],{"class":263},[250,6159,6160],{"class":709},"--zinc-50",[250,6162,5257],{"class":263},[250,6164,6165,6168,6170,6172,6174,6177],{"class":252,"line":1590},[250,6166,6167],{"class":709},"  --foreground",[250,6169,1177],{"class":263},[250,6171,5249],{"class":389},[250,6173,275],{"class":263},[250,6175,6176],{"class":709},"--zinc-900",[250,6178,5257],{"class":263},[250,6180,6181,6184,6186,6188,6190,6193],{"class":252,"line":1600},[250,6182,6183],{"class":709},"  --primary",[250,6185,1177],{"class":263},[250,6187,5249],{"class":389},[250,6189,275],{"class":263},[250,6191,6192],{"class":709},"--cyan-600",[250,6194,5257],{"class":263},[250,6196,6197],{"class":252,"line":1607},[250,6198,287],{"class":263},[250,6200,6201],{"class":252,"line":83},[250,6202,293],{"emptyLinePlaceholder":24},[250,6204,6205,6207,6210,6212,6215],{"class":252,"line":2893},[250,6206,5809],{"class":263},[250,6208,6209],{"class":389},"data-theme",[250,6211,614],{"class":255},[250,6213,6214],{"class":278},"'dark'",[250,6216,6217],{"class":263},"] {\n",[250,6219,6220,6222,6224,6226,6228,6230],{"class":252,"line":2901},[250,6221,6151],{"class":709},[250,6223,1177],{"class":263},[250,6225,5249],{"class":389},[250,6227,275],{"class":263},[250,6229,6176],{"class":709},[250,6231,5257],{"class":263},[250,6233,6234,6236,6238,6240,6242,6244],{"class":252,"line":2911},[250,6235,6167],{"class":709},[250,6237,1177],{"class":263},[250,6239,5249],{"class":389},[250,6241,275],{"class":263},[250,6243,6160],{"class":709},[250,6245,5257],{"class":263},[250,6247,6248,6250,6252,6254,6256,6259],{"class":252,"line":2923},[250,6249,6183],{"class":709},[250,6251,1177],{"class":263},[250,6253,5249],{"class":389},[250,6255,275],{"class":263},[250,6257,6258],{"class":709},"--cyan-400",[250,6260,5257],{"class":263},[250,6262,6263],{"class":252,"line":50},[250,6264,287],{"class":263},[39,6266,6267,6268,6271,6272,6275],{},"খেয়াল করুন ",[247,6269,6270],{},"--primary"," দুই থিমে ",[517,6273,6274],{},"আলাদা hex","। এটা ভুল নয়, এটাই মূল কথা। ডার্ক মোডের উজ্জ্বল সায়ান সাদা ব্যাকগ্রাউন্ডে কনট্রাস্ট পরীক্ষায় ফেল করে। \"একটা ব্র্যান্ড কালার সব জায়গায়\" — এই ধারণাটাই অ্যাক্সেসিবিলিটির সাথে সাংঘর্ষিক।",[39,6277,6278],{},"কম্পোনেন্ট এখন শুধু ভূমিকা ব্যবহার করে:",[241,6280,6282],{"className":4588,"code":6281,"language":4590,"meta":46,"style":46},".card {\n  background: var(--surface);\n  color: var(--foreground);\n}\n",[247,6283,6284,6290,6305,6321],{"__ignoreMap":46},[250,6285,6286,6288],{"class":252,"line":56},[250,6287,5970],{"class":389},[250,6289,1064],{"class":263},[250,6291,6292,6294,6296,6298,6300,6303],{"class":252,"line":203},[250,6293,5977],{"class":389},[250,6295,1177],{"class":263},[250,6297,5249],{"class":389},[250,6299,275],{"class":263},[250,6301,6302],{"class":709},"--surface",[250,6304,5257],{"class":263},[250,6306,6307,6310,6312,6314,6316,6319],{"class":252,"line":47},[250,6308,6309],{"class":389},"  color",[250,6311,1177],{"class":263},[250,6313,5249],{"class":389},[250,6315,275],{"class":263},[250,6317,6318],{"class":709},"--foreground",[250,6320,5257],{"class":263},[250,6322,6323],{"class":252,"line":290},[250,6324,287],{"class":263},[39,6326,6327,6328,6331],{},"রিব্র্যান্ড এখন ",[517,6329,6330],{},"লেয়ার দুইয়ের একটা এডিট","। কম্পোনেন্ট একটাও ছুঁতে হয় না।",[367,6333,6335],{"id":6334},"ভুল-১-টোকেনের-নাম-রঙ-দিয়ে-রাখা","ভুল ১ — টোকেনের নাম রঙ দিয়ে রাখা",[39,6337,6338,6341,6342,6344],{},[247,6339,6340],{},"--brand-blue"," একটা টাইম বোমা। ",[247,6343,6270],{}," নয়।",[39,6346,6347,6348,1687,6350,1687,6353,6356],{},"নিয়মটা সহজ: টোকেনের নামে যদি এমন কিছু থাকে যা রিব্র্যান্ডে মিথ্যা হয়ে যেতে পারে, নামটা ভুল। ",[247,6349,6047],{},[247,6351,6352],{},"large",[247,6354,6355],{},"top-nav"," — সবগুলোই কোনো না কোনো দিন মিথ্যা হয়।",[367,6358,6360],{"id":6359},"ভুল-২-বিল্ড-টাইমে-রিজলভ-করা","ভুল ২ — বিল্ড টাইমে রিজলভ করা",[39,6362,6363],{},"Tailwind v4-এ এইটা নিঃশব্দে কামড় দেয়।",[241,6365,6367],{"className":4588,"code":6366,"language":4590,"meta":46,"style":46},"\u002F* ভুল — টগল কাজ করা বন্ধ করে দেয় *\u002F\n@theme {\n  --color-surface: var(--surface);\n}\n\n\u002F* ঠিক *\u002F\n@theme inline {\n  --color-surface: var(--surface);\n}\n",[247,6368,6369,6374,6381,6386,6390,6394,6399,6406,6410],{"__ignoreMap":46},[250,6370,6371],{"class":252,"line":56},[250,6372,6373],{"class":434},"\u002F* ভুল — টগল কাজ করা বন্ধ করে দেয় *\u002F\n",[250,6375,6376,6379],{"class":252,"line":203},[250,6377,6378],{"class":255},"@theme",[250,6380,1064],{"class":263},[250,6382,6383],{"class":252,"line":47},[250,6384,6385],{"class":263},"  --color-surface: var(--surface);\n",[250,6387,6388],{"class":252,"line":290},[250,6389,287],{"class":263},[250,6391,6392],{"class":252,"line":296},[250,6393,293],{"emptyLinePlaceholder":24},[250,6395,6396],{"class":252,"line":306},[250,6397,6398],{"class":434},"\u002F* ঠিক *\u002F\n",[250,6400,6401,6403],{"class":252,"line":315},[250,6402,6378],{"class":255},[250,6404,6405],{"class":263}," inline {\n",[250,6407,6408],{"class":252,"line":329},[250,6409,6385],{"class":263},[250,6411,6412],{"class":252,"line":334},[250,6413,287],{"class":263},[39,6415,6416,6417,6419,6420,6423,6424,6427],{},"সাধারণ ",[247,6418,6378],{}," ব্লক মানটা বিল্ড টাইমে রিজলভ করে ফেলে — অর্থাৎ ",[247,6421,6422],{},"bg-surface"," কম্পাইল হয়ে একটা fixed hex-এ পরিণত হয়। থিম অ্যাট্রিবিউট বদলালে ভেরিয়েবল বদলায়, কিন্তু ক্লাসটা আর ভেরিয়েবল পড়ছেই না। ",[247,6425,6426],{},"inline"," কি-ওয়ার্ডটা রেফারেন্সটা রানটাইম পর্যন্ত টিকিয়ে রাখে।",[39,6429,6430],{},"লক্ষণটা বিভ্রান্তিকর: থিম টগল \"কাজ করে না\", অথচ ভেরিয়েবলগুলো DevTools-এ ঠিকঠাক বদলাচ্ছে।",[367,6432,6434],{"id":6433},"ভুল-৩-অনেক-বেশি-টোকেন","ভুল ৩ — অনেক বেশি টোকেন",[39,6436,6437],{},"একটা সিস্টেমে ২০০টা স্পেসিং টোকেন থাকলে সেটা সিস্টেম নয়, সেটা একটা অভিধান। কেউ মুখস্থ করবে না, সবাই কাছাকাছি একটা বেছে নেবে, আর ফলাফল হবে ইনকনসিস্টেন্সি — ঠিক যেটা ঠেকাতে সিস্টেমটা বানানো।",[39,6439,6440],{},"কম টোকেন, কড়া নিয়ম। যদি একটা মান বছরে একবার লাগে, সেটা টোকেন নয় — সেটা ওই এক জায়গার একটা মান।",[367,6442,6444],{"id":6443},"ভুল-৪-কম্পোনেন্ট-api-টা-লিখে-না-রাখা","ভুল ৪ — কম্পোনেন্ট API-টা লিখে না রাখা",[39,6446,6447],{},"টোকেন ঠিক থাকলেও কম্পোনেন্ট যদি যা খুশি ক্লাস নেয়, সিস্টেমটা ফাঁকা। প্রতিটা ভ্যারিয়েন্ট এক জায়গায় ঘোষিত থাকা দরকার:",[241,6449,6451],{"className":5026,"code":6450,"language":5028,"meta":46,"style":46},"const button = tv({\n  base: 'inline-flex items-center justify-center rounded-lg',\n  variants: {\n    variant: {\n      primary: 'bg-primary text-on-primary',\n      ghost: 'bg-transparent hover:bg-surface-hover',\n    },\n    size: { sm: 'h-8 px-3', md: 'h-10 px-4' },\n  },\n})\n",[247,6452,6453,6468,6478,6483,6488,6498,6508,6513,6529,6534],{"__ignoreMap":46},[250,6454,6455,6457,6460,6462,6465],{"class":252,"line":56},[250,6456,386],{"class":255},[250,6458,6459],{"class":389}," button",[250,6461,393],{"class":255},[250,6463,6464],{"class":259}," tv",[250,6466,6467],{"class":263},"({\n",[250,6469,6470,6473,6476],{"class":252,"line":203},[250,6471,6472],{"class":263},"  base: ",[250,6474,6475],{"class":278},"'inline-flex items-center justify-center rounded-lg'",[250,6477,5097],{"class":263},[250,6479,6480],{"class":252,"line":47},[250,6481,6482],{"class":263},"  variants: {\n",[250,6484,6485],{"class":252,"line":290},[250,6486,6487],{"class":263},"    variant: {\n",[250,6489,6490,6493,6496],{"class":252,"line":296},[250,6491,6492],{"class":263},"      primary: ",[250,6494,6495],{"class":278},"'bg-primary text-on-primary'",[250,6497,5097],{"class":263},[250,6499,6500,6503,6506],{"class":252,"line":306},[250,6501,6502],{"class":263},"      ghost: ",[250,6504,6505],{"class":278},"'bg-transparent hover:bg-surface-hover'",[250,6507,5097],{"class":263},[250,6509,6510],{"class":252,"line":315},[250,6511,6512],{"class":263},"    },\n",[250,6514,6515,6518,6521,6524,6527],{"class":252,"line":329},[250,6516,6517],{"class":263},"    size: { sm: ",[250,6519,6520],{"class":278},"'h-8 px-3'",[250,6522,6523],{"class":263},", md: ",[250,6525,6526],{"class":278},"'h-10 px-4'",[250,6528,5081],{"class":263},[250,6530,6531],{"class":252,"line":334},[250,6532,6533],{"class":263},"  },\n",[250,6535,6536],{"class":252,"line":339},[250,6537,6538],{"class":263},"})\n",[39,6540,6541,6542,6545],{},"টেবিলে না থাকলে ভ্যারিয়েন্টটা ",[517,6543,6544],{},"নেই","। এটাই একমাত্র নিয়ম যেটা সিস্টেমকে দুই বছর পরেও সিস্টেম রাখে।",[367,6547,6549],{"id":6548},"যাচাই-করার-উপায়","যাচাই করার উপায়",[39,6551,6552,6553],{},"সিস্টেমটা টিকবে কি না, তার একটাই সৎ পরীক্ষা: ",[517,6554,6555],{},"প্রাইমারি রঙটা বদলে দিন।",[39,6557,6558],{},"একটা ফাইলে একটা লাইন বদলে যদি পুরো প্রোডাক্ট বদলে যায় — দুই থিমেই, কনট্রাস্ট ভেঙে না গিয়ে — তাহলে তিনটা লেয়ার ঠিক আছে। যদি খুঁজে খুঁজে বদলাতে হয়, তাহলে এখনো দুইটা লেয়ার, আর প্রথম রিব্র্যান্ডেই এটা ভাঙবে।",[39,6560,6561],{},"এই পরীক্ষাটা করতে এক দিন লাগে। রিব্র্যান্ডের পর করতে হলে এক কোয়ার্টার লাগে।",[477,6563,6564],{},"html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .siJwO, html code.shiki .siJwO{--shiki-default:#F69D50;--shiki-dark:#F69D50}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}",{"title":46,"searchDepth":47,"depth":47,"links":6566},[6567,6568,6569,6570,6571,6572,6573],{"id":5917,"depth":203,"text":5918},{"id":6057,"depth":203,"text":6058},{"id":6334,"depth":203,"text":6335},{"id":6359,"depth":203,"text":6360},{"id":6433,"depth":203,"text":6434},{"id":6443,"depth":203,"text":6444},{"id":6548,"depth":203,"text":6549},"design-systems","\u002Fcovers\u002Fblog\u002Fbn\u002Fdesign-system-mistakes.webp","বেশিরভাগ টোকেন সিস্টেম প্রথম রিব্র্যান্ডেই ভেঙে পড়ে। কারণটা প্রায় সবসময় এক — দুইটা লেয়ার আছে, দরকার ছিল তিনটা।",{},"\u002Fblog\u002Fbn\u002Fdesign-system-mistakes",{"title":5906,"description":6576},"blog\u002Fbn\u002Fdesign-system-mistakes",[6582,6583,6584],"ডিজাইন সিস্টেম","টোকেন","Tailwind","_t10HyFYNMKbFM1KZ69e8oB9eMINx6xpy20k22vHQ5s",{"id":6587,"title":6588,"author":6589,"body":6592,"category":6574,"cover":7159,"coverAlt":6588,"date":7160,"description":7161,"draft":6,"extension":51,"featured":24,"lang":110,"meta":7162,"navigation":24,"path":7163,"readingTime":315,"seo":7164,"stem":7165,"tags":7166,"__hash__":7169},"blog\u002Fblog\u002Fen\u002Fdesign-tokens-that-survive-a-rebrand.md","Design tokens that survive a rebrand",{"name":6590,"role":6591},"Elena Marsh","Design Systems Lead",{"type":36,"value":6593,"toc":7151},[6594,6597,6601,6604,6681,6684,6698,6704,6708,6718,6983,6986,6996,7000,7003,7056,7060,7069,7098,7118,7126,7130,7133,7138,7141,7145,7148],[39,6595,6596],{},"A design system survives its first rebrand or it does not. Most do not, and the reason is almost always the same: the system has two layers where it needs three.",[367,6598,6600],{"id":6599},"the-two-layer-trap","The two-layer trap",[39,6602,6603],{},"Here is the shape nearly every token system starts with. A palette, and components that consume it.",[241,6605,6607],{"className":4588,"code":6606,"language":4590,"meta":46,"style":46},":root {\n  --blue-500: #00d4ff;\n  --gray-900: #18181b;\n}\n\n.card {\n  background: var(--gray-900);\n  border-color: var(--blue-500);\n}\n",[247,6608,6609,6615,6625,6635,6639,6643,6649,6663,6677],{"__ignoreMap":46},[250,6610,6611,6613],{"class":252,"line":56},[250,6612,5931],{"class":389},[250,6614,1064],{"class":263},[250,6616,6617,6619,6621,6623],{"class":252,"line":203},[250,6618,5938],{"class":709},[250,6620,1177],{"class":263},[250,6622,5943],{"class":389},[250,6624,4615],{"class":263},[250,6626,6627,6629,6631,6633],{"class":252,"line":47},[250,6628,5950],{"class":709},[250,6630,1177],{"class":263},[250,6632,5955],{"class":389},[250,6634,4615],{"class":263},[250,6636,6637],{"class":252,"line":290},[250,6638,287],{"class":263},[250,6640,6641],{"class":252,"line":296},[250,6642,293],{"emptyLinePlaceholder":24},[250,6644,6645,6647],{"class":252,"line":306},[250,6646,5970],{"class":389},[250,6648,1064],{"class":263},[250,6650,6651,6653,6655,6657,6659,6661],{"class":252,"line":315},[250,6652,5977],{"class":389},[250,6654,1177],{"class":263},[250,6656,5249],{"class":389},[250,6658,275],{"class":263},[250,6660,5986],{"class":709},[250,6662,5257],{"class":263},[250,6664,6665,6667,6669,6671,6673,6675],{"class":252,"line":329},[250,6666,5993],{"class":389},[250,6668,1177],{"class":263},[250,6670,5249],{"class":389},[250,6672,275],{"class":263},[250,6674,6032],{"class":709},[250,6676,5257],{"class":263},[250,6678,6679],{"class":252,"line":334},[250,6680,287],{"class":263},[39,6682,6683],{},"This is already better than hardcoded hexes, and it feels like the job is done. It is not, and the failure mode is specific.",[39,6685,6686,6687,6689,6690,6693,6694,6697],{},"Six months in, someone asks for a light theme. ",[247,6688,5986],{}," is the card background in dark mode and needs to be near-white in light mode. So you either rename the variable to something that is now a lie, or you write ",[247,6691,6692],{},"--gray-900: #ffffff"," under ",[247,6695,6696],{},"[data-theme=\"light\"]",", which is worse.",[39,6699,6700,6701,6703],{},"Then the rebrand lands, blue becomes violet, and you discover ",[247,6702,6032],{}," is referenced in 340 places — some of which meant \"our brand colour\" and some of which meant \"the colour of a link\" and some of which meant \"this particular chart series.\" Those are three different concepts that happened to share a hex.",[367,6705,6707],{"id":6706},"the-layer-you-are-missing","The layer you are missing",[39,6709,6710,6711,6713,6714,6717],{},"Primitives describe what a value ",[947,6712,648],{},". Semantics describe what it is ",[947,6715,6716],{},"for",". You need both, and components may only touch the second.",[241,6719,6721],{"className":4588,"code":6720,"language":4590,"meta":46,"style":46},"\u002F* 1. Primitives — raw values, no product meaning *\u002F\n:root {\n  --cyan-500: #00d4ff;\n  --neutral-900: #18181b;\n  --neutral-0: #ffffff;\n}\n\n\u002F* 2. Semantics — roles, which flip per theme *\u002F\n:root,\n[data-theme='dark'] {\n  --surface: var(--neutral-900);\n  --primary: var(--cyan-500);\n  --border: rgb(255 255 255 \u002F 0.08);\n}\n\n[data-theme='light'] {\n  --surface: var(--neutral-0);\n  --primary: #0093b8;\n  --border: rgb(9 9 11 \u002F 0.09);\n}\n\n\u002F* 3. Components — roles only *\u002F\n.card {\n  background: var(--surface);\n  border-color: var(--border);\n}\n",[247,6722,6723,6728,6734,6745,6756,6768,6772,6776,6781,6787,6799,6814,6829,6857,6861,6865,6878,6893,6904,6930,6934,6938,6943,6949,6963,6978],{"__ignoreMap":46},[250,6724,6725],{"class":252,"line":56},[250,6726,6727],{"class":434},"\u002F* 1. Primitives — raw values, no product meaning *\u002F\n",[250,6729,6730,6732],{"class":252,"line":203},[250,6731,5931],{"class":389},[250,6733,1064],{"class":263},[250,6735,6736,6739,6741,6743],{"class":252,"line":47},[250,6737,6738],{"class":709},"  --cyan-500",[250,6740,1177],{"class":263},[250,6742,5943],{"class":389},[250,6744,4615],{"class":263},[250,6746,6747,6750,6752,6754],{"class":252,"line":290},[250,6748,6749],{"class":709},"  --neutral-900",[250,6751,1177],{"class":263},[250,6753,5955],{"class":389},[250,6755,4615],{"class":263},[250,6757,6758,6761,6763,6766],{"class":252,"line":296},[250,6759,6760],{"class":709},"  --neutral-0",[250,6762,1177],{"class":263},[250,6764,6765],{"class":389},"#ffffff",[250,6767,4615],{"class":263},[250,6769,6770],{"class":252,"line":306},[250,6771,287],{"class":263},[250,6773,6774],{"class":252,"line":315},[250,6775,293],{"emptyLinePlaceholder":24},[250,6777,6778],{"class":252,"line":329},[250,6779,6780],{"class":434},"\u002F* 2. Semantics — roles, which flip per theme *\u002F\n",[250,6782,6783,6785],{"class":252,"line":334},[250,6784,5931],{"class":389},[250,6786,5097],{"class":263},[250,6788,6789,6791,6793,6795,6797],{"class":252,"line":339},[250,6790,5809],{"class":263},[250,6792,6209],{"class":389},[250,6794,614],{"class":255},[250,6796,6214],{"class":278},[250,6798,6217],{"class":263},[250,6800,6801,6803,6805,6807,6809,6812],{"class":252,"line":864},[250,6802,6151],{"class":709},[250,6804,1177],{"class":263},[250,6806,5249],{"class":389},[250,6808,275],{"class":263},[250,6810,6811],{"class":709},"--neutral-900",[250,6813,5257],{"class":263},[250,6815,6816,6818,6820,6822,6824,6827],{"class":252,"line":1590},[250,6817,6183],{"class":709},[250,6819,1177],{"class":263},[250,6821,5249],{"class":389},[250,6823,275],{"class":263},[250,6825,6826],{"class":709},"--cyan-500",[250,6828,5257],{"class":263},[250,6830,6831,6834,6836,6839,6841,6844,6847,6849,6852,6855],{"class":252,"line":1600},[250,6832,6833],{"class":709},"  --border",[250,6835,1177],{"class":263},[250,6837,6838],{"class":389},"rgb",[250,6840,275],{"class":263},[250,6842,6843],{"class":389},"255",[250,6845,6846],{"class":389}," 255",[250,6848,6846],{"class":389},[250,6850,6851],{"class":263}," \u002F ",[250,6853,6854],{"class":389},"0.08",[250,6856,5257],{"class":263},[250,6858,6859],{"class":252,"line":1607},[250,6860,287],{"class":263},[250,6862,6863],{"class":252,"line":83},[250,6864,293],{"emptyLinePlaceholder":24},[250,6866,6867,6869,6871,6873,6876],{"class":252,"line":2893},[250,6868,5809],{"class":263},[250,6870,6209],{"class":389},[250,6872,614],{"class":255},[250,6874,6875],{"class":278},"'light'",[250,6877,6217],{"class":263},[250,6879,6880,6882,6884,6886,6888,6891],{"class":252,"line":2901},[250,6881,6151],{"class":709},[250,6883,1177],{"class":263},[250,6885,5249],{"class":389},[250,6887,275],{"class":263},[250,6889,6890],{"class":709},"--neutral-0",[250,6892,5257],{"class":263},[250,6894,6895,6897,6899,6902],{"class":252,"line":2911},[250,6896,6183],{"class":709},[250,6898,1177],{"class":263},[250,6900,6901],{"class":389},"#0093b8",[250,6903,4615],{"class":263},[250,6905,6906,6908,6910,6912,6914,6917,6920,6923,6925,6928],{"class":252,"line":2923},[250,6907,6833],{"class":709},[250,6909,1177],{"class":263},[250,6911,6838],{"class":389},[250,6913,275],{"class":263},[250,6915,6916],{"class":389},"9",[250,6918,6919],{"class":389}," 9",[250,6921,6922],{"class":389}," 11",[250,6924,6851],{"class":263},[250,6926,6927],{"class":389},"0.09",[250,6929,5257],{"class":263},[250,6931,6932],{"class":252,"line":50},[250,6933,287],{"class":263},[250,6935,6936],{"class":252,"line":2938},[250,6937,293],{"emptyLinePlaceholder":24},[250,6939,6940],{"class":252,"line":2945},[250,6941,6942],{"class":434},"\u002F* 3. Components — roles only *\u002F\n",[250,6944,6945,6947],{"class":252,"line":2954},[250,6946,5970],{"class":389},[250,6948,1064],{"class":263},[250,6950,6951,6953,6955,6957,6959,6961],{"class":252,"line":5},[250,6952,5977],{"class":389},[250,6954,1177],{"class":263},[250,6956,5249],{"class":389},[250,6958,275],{"class":263},[250,6960,6302],{"class":709},[250,6962,5257],{"class":263},[250,6964,6965,6967,6969,6971,6973,6976],{"class":252,"line":170},[250,6966,5993],{"class":389},[250,6968,1177],{"class":263},[250,6970,5249],{"class":389},[250,6972,275],{"class":263},[250,6974,6975],{"class":709},"--border",[250,6977,5257],{"class":263},[250,6979,6981],{"class":252,"line":6980},26,[250,6982,287],{"class":263},[39,6984,6985],{},"Now the light theme is a block of overrides in one place, and the rebrand is an edit to the semantic layer. The 340 call sites do not move, because they were never asking for blue. They were asking for \"primary,\" and primary is now violet.",[39,6987,6988,6989,6991,6992,6995],{},"Note also that ",[247,6990,6270],{}," is not the same hex in both themes. ",[247,6993,6994],{},"#00D4FF"," on white fails contrast badly; the light theme needs a darker cyan to clear WCAG AA. A two-layer system cannot express that without a conditional at every call site. A three-layer system expresses it once.",[367,6997,6999],{"id":6998},"naming-the-semantic-layer","Naming the semantic layer",[39,7001,7002],{},"The semantic layer is only useful if the names describe roles rather than appearance. Some rules that hold up:",[2038,7004,7005,7024,7030,7046],{},[514,7006,7007,7010,7011,7014,7015,7017,7018,7014,7021,1886],{},[517,7008,7009],{},"Name the job, not the look."," ",[247,7012,7013],{},"--surface-raised",", not ",[247,7016,6002],{},". ",[247,7019,7020],{},"--danger",[247,7022,7023],{},"--red",[514,7025,7026,7029],{},[517,7027,7028],{},"Pair foreground with background."," Every surface token gets a matching foreground token, checked for contrast once, so no component has to think about it.",[514,7031,7032,7010,7035,1687,7038,7041,7042,7045],{},[517,7033,7034],{},"Encode state in the name.",[247,7036,7037],{},"--surface-hover",[247,7039,7040],{},"--surface-active",". If hover is derived at the call site with ",[247,7043,7044],{},"color-mix"," or an opacity modifier, you have leaked appearance logic back into components.",[514,7047,7048,7051,7052,7055],{},[517,7049,7050],{},"Resist the urge to add a third brand colour."," Most systems that end up with ",[247,7053,7054],{},"--tertiary"," did so because someone needed a specific shade once. That is what a one-off class is for.",[367,7057,7059],{"id":7058},"wiring-it-to-tailwind-v4","Wiring it to Tailwind v4",[39,7061,7062,7063,7065,7066,7068],{},"Tailwind v4's ",[247,7064,6378],{}," generates utilities from CSS variables, and it has an ",[247,7067,6426],{}," variant that matters here.",[241,7070,7072],{"className":4588,"code":7071,"language":4590,"meta":46,"style":46},"@theme inline {\n  --color-surface: var(--surface);\n  --color-primary: var(--primary);\n  --color-border: var(--border);\n}\n",[247,7073,7074,7080,7084,7089,7094],{"__ignoreMap":46},[250,7075,7076,7078],{"class":252,"line":56},[250,7077,6378],{"class":255},[250,7079,6405],{"class":263},[250,7081,7082],{"class":252,"line":203},[250,7083,6385],{"class":263},[250,7085,7086],{"class":252,"line":47},[250,7087,7088],{"class":263},"  --color-primary: var(--primary);\n",[250,7090,7091],{"class":252,"line":290},[250,7092,7093],{"class":263},"  --color-border: var(--border);\n",[250,7095,7096],{"class":252,"line":296},[250,7097,287],{"class":263},[39,7099,7100,7103,7104,7107,7108,7110,7111,7114,7115,7117],{},[247,7101,7102],{},"@theme inline"," keeps the ",[247,7105,7106],{},"var()"," reference live in the generated utility, so ",[247,7109,6422],{}," emits ",[247,7112,7113],{},"background-color: var(--surface)"," and re-resolves when the theme attribute changes. Without ",[247,7116,6426],{},", Tailwind resolves the value at build time and your theme switch silently does nothing — a bug that is genuinely unpleasant to track down because the CSS looks correct.",[39,7119,7120,7121,7123,7124,1886],{},"Static scales that never flip — spacing, radius, duration, easing — go in a plain ",[247,7122,6378],{}," block. Only the colour roles need ",[247,7125,6426],{},[367,7127,7129],{"id":7128},"the-test","The test",[39,7131,7132],{},"There is one question that tells you whether your token layer is real:",[1761,7134,7135],{},[39,7136,7137],{},"If the brand colour changed tomorrow, how many files would you edit?",[39,7139,7140],{},"If the answer is more than one, you have a naming convention, not an architecture. The gap between those two is roughly a day of work, and it is the difference between a rebrand being a variable change and a rebrand being a quarter.",[367,7142,7144],{"id":7143},"where-this-stops-being-worth-it","Where this stops being worth it",[39,7146,7147],{},"For a marketing site with six components, three layers is overhead. The break-even is somewhere around the point where you have a second theme, a second product, or a second team — whichever arrives first. Below that, a flat palette is fine and you should not let anyone tell you otherwise.",[477,7149,7150],{},"html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .siJwO, html code.shiki .siJwO{--shiki-default:#F69D50;--shiki-dark:#F69D50}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}",{"title":46,"searchDepth":47,"depth":47,"links":7152},[7153,7154,7155,7156,7157,7158],{"id":6599,"depth":203,"text":6600},{"id":6706,"depth":203,"text":6707},{"id":6998,"depth":203,"text":6999},{"id":7058,"depth":203,"text":7059},{"id":7128,"depth":203,"text":7129},{"id":7143,"depth":203,"text":7144},"\u002Fcovers\u002Fblog\u002Fen\u002Fdesign-tokens-that-survive-a-rebrand.webp","2026-06-18","Most token systems break the first time the brand changes. The fix is a layer most teams skip — and it costs about a day to add.",{},"\u002Fblog\u002Fen\u002Fdesign-tokens-that-survive-a-rebrand",{"title":6588,"description":7161},"blog\u002Fen\u002Fdesign-tokens-that-survive-a-rebrand",[7167,7168,6584],"Design Tokens","Architecture","7dPa213uoSmziFZ1uEs730NPYoCBRpGHOQQ_ovl_Bs0",{"id":7171,"title":7172,"author":7173,"body":7174,"category":3846,"cover":7846,"coverAlt":7172,"date":7847,"description":7848,"draft":6,"extension":51,"featured":6,"lang":53,"meta":7849,"navigation":24,"path":7850,"readingTime":315,"seo":7851,"stem":7852,"tags":7853,"__hash__":7855},"blog\u002Fblog\u002Fbn\u002Fevent-loop-settimeout-zero.md","setTimeout(fn, 0) সাথে সাথে চলে না কেন",{"name":5908,"role":5909},{"type":36,"value":7175,"toc":7838},[7176,7179,7267,7276,7279,7283,7286,7300,7303,7347,7350,7354,7357,7362,7388,7394,7413,7416,7425,7433,7437,7444,7487,7493,7499,7536,7539,7543,7552,7632,7645,7650,7654,7657,7663,7666,7672,7800,7812,7816,7826,7835],[39,7177,7178],{},"এই কোডটার আউটপুট কী হবে?",[241,7180,7181],{"className":243,"code":1832,"language":245,"meta":46,"style":46},[247,7182,7183,7195,7199,7221,7225,7251,7255],{"__ignoreMap":46},[250,7184,7185,7187,7189,7191,7193],{"class":252,"line":56},[250,7186,971],{"class":263},[250,7188,272],{"class":259},[250,7190,275],{"class":263},[250,7192,1845],{"class":278},[250,7194,282],{"class":263},[250,7196,7197],{"class":252,"line":203},[250,7198,293],{"emptyLinePlaceholder":24},[250,7200,7201,7203,7205,7207,7209,7211,7213,7215,7217,7219],{"class":252,"line":47},[250,7202,956],{"class":259},[250,7204,987],{"class":263},[250,7206,990],{"class":255},[250,7208,993],{"class":263},[250,7210,272],{"class":259},[250,7212,275],{"class":263},[250,7214,1868],{"class":278},[250,7216,1003],{"class":263},[250,7218,1006],{"class":389},[250,7220,282],{"class":263},[250,7222,7223],{"class":252,"line":290},[250,7224,293],{"emptyLinePlaceholder":24},[250,7226,7227,7229,7231,7233,7235,7237,7239,7241,7243,7245,7247,7249],{"class":252,"line":296},[250,7228,1883],{"class":389},[250,7230,1886],{"class":263},[250,7232,1889],{"class":259},[250,7234,1892],{"class":263},[250,7236,1895],{"class":259},[250,7238,987],{"class":263},[250,7240,990],{"class":255},[250,7242,993],{"class":263},[250,7244,272],{"class":259},[250,7246,275],{"class":263},[250,7248,1908],{"class":278},[250,7250,1911],{"class":263},[250,7252,7253],{"class":252,"line":306},[250,7254,293],{"emptyLinePlaceholder":24},[250,7256,7257,7259,7261,7263,7265],{"class":252,"line":315},[250,7258,971],{"class":263},[250,7260,272],{"class":259},[250,7262,275],{"class":263},[250,7264,1926],{"class":278},[250,7266,282],{"class":263},[39,7268,7269,7270,358,7273,7275],{},"উত্তর ",[247,7271,7272],{},"১ ৪ ৩ ২",[247,7274,956],{},"-এ ০ মিলিসেকেন্ড দেওয়া সত্ত্বেও সেটা সবার শেষে চলে, এমনকি এমন একটা প্রমিসের পরেও যেটা তার পরে লেখা হয়েছে।",[39,7277,7278],{},"\"০ মানে এখনই নয়\" — এইটুকু মনে রাখলে কাজ চলে যায়, কিন্তু কেন নয় সেটা জানলে আর মুখস্থ করতে হয় না।",[367,7280,7282],{"id":7281},"কল-স্ট্যাক-আগে-শেষ-হয়","কল স্ট্যাক আগে শেষ হয়",[39,7284,7285],{},"জাভাস্ক্রিপ্ট সিঙ্গল-থ্রেডেড। যেকোনো মুহূর্তে একটাই জিনিস চলে, আর সেটা কল স্ট্যাকের উপরের ফাংশনটা।",[39,7287,7288,7289,7292,7293,1949,7296,7299],{},"ইভেন্ট লুপ কোনো কিছু তুলে নেওয়ার আগে ",[517,7290,7291],{},"স্ট্যাক পুরো খালি হওয়া লাগে","। তাই উপরের কোডে ",[247,7294,7295],{},"console.log('১')",[247,7297,7298],{},"console.log('৪')"," — যেগুলো সিঙ্ক্রোনাস — আগে চলবেই। বাকি সবকিছু সারিতে অপেক্ষা করে।",[39,7301,7302],{},"এখান থেকেই সবচেয়ে সাধারণ বাগটা আসে:",[241,7304,7306],{"className":243,"code":7305,"language":245,"meta":46,"style":46},"\u002F\u002F ব্রাউজার এই পুরো লুপটা শেষ না হওয়া পর্যন্ত জমে থাকে\nfor (let i = 0; i \u003C 1e9; i++) {}\n",[247,7307,7308,7313],{"__ignoreMap":46},[250,7309,7310],{"class":252,"line":56},[250,7311,7312],{"class":434},"\u002F\u002F ব্রাউজার এই পুরো লুপটা শেষ না হওয়া পর্যন্ত জমে থাকে\n",[250,7314,7315,7317,7320,7323,7326,7328,7331,7334,7336,7339,7342,7345],{"class":252,"line":203},[250,7316,6716],{"class":255},[250,7318,7319],{"class":263}," (",[250,7321,7322],{"class":255},"let",[250,7324,7325],{"class":263}," i ",[250,7327,614],{"class":255},[250,7329,7330],{"class":389}," 0",[250,7332,7333],{"class":263},"; i ",[250,7335,423],{"class":255},[250,7337,7338],{"class":389}," 1e9",[250,7340,7341],{"class":263},"; i",[250,7343,7344],{"class":255},"++",[250,7346,928],{"class":263},[39,7348,7349],{},"স্ট্যাক খালি না হলে ইভেন্ট লুপ কিছুই করতে পারে না — ক্লিক হ্যান্ডলার চলবে না, রেন্ডারও হবে না। \"পেজ হ্যাং করে আছে\" মানে বেশিরভাগ সময় \"স্ট্যাক খালি হচ্ছে না\"।",[367,7351,7353],{"id":7352},"দুইটা-আলাদা-সারি","দুইটা আলাদা সারি",[39,7355,7356],{},"স্ট্যাক খালি হলে ইভেন্ট লুপ যা দেখে, তা একটা সারি নয় — দুইটা, আর তাদের অগ্রাধিকার আলাদা।",[39,7358,7359],{},[517,7360,7361],{},"মাইক্রোটাস্ক সারি",[2038,7363,7364,7375,7380,7384],{},[514,7365,7366,6851,7369,6851,7372],{},[247,7367,7368],{},"Promise.then",[247,7370,7371],{},".catch",[247,7373,7374],{},".finally",[514,7376,7377,7379],{},[247,7378,1113],{},"-এর পরের অংশ",[514,7381,7382],{},[247,7383,1800],{},[514,7385,7386],{},[247,7387,1803],{},[39,7389,7390,7393],{},[517,7391,7392],{},"ম্যাক্রোটাস্ক সারি"," (স্পেকে \"টাস্ক\")",[2038,7395,7396,7402,7405,7408],{},[514,7397,7398,1687,7400],{},[247,7399,956],{},[247,7401,1818],{},[514,7403,7404],{},"DOM ইভেন্ট (click, input)",[514,7406,7407],{},"নেটওয়ার্ক কলব্যাক",[514,7409,7410,7412],{},[247,7411,2700],{}," (আলাদা ধাপে, রেন্ডারের আগে)",[39,7414,7415],{},"নিয়মটা এক লাইনে:",[1761,7417,7418],{},[39,7419,7420,7421,7424],{},"প্রতিটা ম্যাক্রোটাস্কের পর, ",[517,7422,7423],{},"পুরো"," মাইক্রোটাস্ক সারি খালি করা হয় — তারপর পরের ম্যাক্রোটাস্ক।",[39,7426,4455,7427,7429,7430,7432],{},[247,7428,1940],{}," (প্রমিস, মাইক্রোটাস্ক) ",[247,7431,1943],{},"-এর (টাইমআউট, ম্যাক্রোটাস্ক) আগে চলে। ০ মিলিসেকেন্ড লেখা থাকলেও সেটা অন্য সারিতে, আর ওই সারির পালা পরে।",[367,7434,7436],{"id":7435},"পুরো-শব্দটা-গুরুত্বপূর্ণ","\"পুরো\" শব্দটা গুরুত্বপূর্ণ",[39,7438,7439,7440,7443],{},"মাইক্রোটাস্ক সারি খালি করার সময় নতুন মাইক্রোটাস্ক যোগ হলে সেগুলোও ",[517,7441,7442],{},"একই দফায়"," চলে। এটা সুবিধা, আবার ফাঁদও:",[241,7445,7447],{"className":243,"code":7446,"language":245,"meta":46,"style":46},"function loop() {\n  Promise.resolve().then(loop)\n}\nloop() \u002F\u002F পেজ জমে যায়\n",[247,7448,7449,7458,7473,7477],{"__ignoreMap":46},[250,7450,7451,7453,7456],{"class":252,"line":56},[250,7452,256],{"class":255},[250,7454,7455],{"class":259}," loop",[250,7457,264],{"class":263},[250,7459,7460,7462,7464,7466,7468,7470],{"class":252,"line":203},[250,7461,1995],{"class":389},[250,7463,1886],{"class":263},[250,7465,1889],{"class":259},[250,7467,1892],{"class":263},[250,7469,1895],{"class":259},[250,7471,7472],{"class":263},"(loop)\n",[250,7474,7475],{"class":252,"line":47},[250,7476,287],{"class":263},[250,7478,7479,7482,7484],{"class":252,"line":290},[250,7480,7481],{"class":259},"loop",[250,7483,414],{"class":263},[250,7485,7486],{"class":434},"\u002F\u002F পেজ জমে যায়\n",[39,7488,7489,7490,7492],{},"এখানে কোনো ইনফিনিট ",[247,7491,406],{}," নেই, তবু ব্রাউজার আর কিছু করতে পারে না — সারিটা কখনো খালি হয় না, তাই লুপ কখনো পরের ধাপে যায় না।",[39,7494,7495,7496,7498],{},"একই কোড ",[247,7497,956],{}," দিয়ে লিখলে পেজ সচল থাকে:",[241,7500,7502],{"className":243,"code":7501,"language":245,"meta":46,"style":46},"function loop() {\n  setTimeout(loop, 0)\n}\nloop() \u002F\u002F চলতেই থাকে, কিন্তু পেজ ব্যবহারযোগ্য\n",[247,7503,7504,7512,7523,7527],{"__ignoreMap":46},[250,7505,7506,7508,7510],{"class":252,"line":56},[250,7507,256],{"class":255},[250,7509,7455],{"class":259},[250,7511,264],{"class":263},[250,7513,7514,7516,7519,7521],{"class":252,"line":203},[250,7515,1057],{"class":259},[250,7517,7518],{"class":263},"(loop, ",[250,7520,1006],{"class":389},[250,7522,282],{"class":263},[250,7524,7525],{"class":252,"line":47},[250,7526,287],{"class":263},[250,7528,7529,7531,7533],{"class":252,"line":290},[250,7530,7481],{"class":259},[250,7532,414],{"class":263},[250,7534,7535],{"class":434},"\u002F\u002F চলতেই থাকে, কিন্তু পেজ ব্যবহারযোগ্য\n",[39,7537,7538],{},"কারণ প্রতিটা ম্যাক্রোটাস্কের মাঝে ব্রাউজার রেন্ডার করার সুযোগ পায়।",[367,7540,7542],{"id":7541},"await-আসলে-কী","await আসলে কী",[39,7544,7545,7547,7548,7551],{},[247,7546,1113],{}," নতুন কিছু নয় — এটা ",[247,7549,7550],{},".then","-এর সিনট্যাক্স।",[241,7553,7555],{"className":243,"code":7554,"language":245,"meta":46,"style":46},"async function run() {\n  console.log('ক')\n  await null \u002F\u002F ← এখানেই ফাংশনটা থামে\n  console.log('খ') \u002F\u002F ← এই অংশটা মাইক্রোটাস্ক\n}\n\nrun()\nconsole.log('গ')\n",[247,7556,7557,7567,7580,7589,7605,7609,7613,7619],{"__ignoreMap":46},[250,7558,7559,7561,7563,7565],{"class":252,"line":56},[250,7560,156],{"class":255},[250,7562,2530],{"class":255},[250,7564,2533],{"class":259},[250,7566,264],{"class":263},[250,7568,7569,7571,7573,7575,7578],{"class":252,"line":203},[250,7570,269],{"class":263},[250,7572,272],{"class":259},[250,7574,275],{"class":263},[250,7576,7577],{"class":278},"'ক'",[250,7579,282],{"class":263},[250,7581,7582,7584,7586],{"class":252,"line":47},[250,7583,2553],{"class":255},[250,7585,2556],{"class":389},[250,7587,7588],{"class":434}," \u002F\u002F ← এখানেই ফাংশনটা থামে\n",[250,7590,7591,7593,7595,7597,7600,7602],{"class":252,"line":290},[250,7592,269],{"class":263},[250,7594,272],{"class":259},[250,7596,275],{"class":263},[250,7598,7599],{"class":278},"'খ'",[250,7601,5217],{"class":263},[250,7603,7604],{"class":434},"\u002F\u002F ← এই অংশটা মাইক্রোটাস্ক\n",[250,7606,7607],{"class":252,"line":296},[250,7608,287],{"class":263},[250,7610,7611],{"class":252,"line":306},[250,7612,293],{"emptyLinePlaceholder":24},[250,7614,7615,7617],{"class":252,"line":315},[250,7616,2585],{"class":259},[250,7618,312],{"class":263},[250,7620,7621,7623,7625,7627,7630],{"class":252,"line":329},[250,7622,971],{"class":263},[250,7624,272],{"class":259},[250,7626,275],{"class":263},[250,7628,7629],{"class":278},"'গ'",[250,7631,282],{"class":263},[39,7633,7634,7635,358,7638,7640,7641,7644],{},"আউটপুট ",[247,7636,7637],{},"ক গ খ",[247,7639,2610],{}," — যেখানে অপেক্ষা করার মতো কিছুই নেই — তবু ফাংশনটা থামে, বাকিটা মাইক্রোটাস্ক হিসেবে সারিতে যায়, আর ",[247,7642,7643],{},"গ"," আগে চলে।",[39,7646,4455,7647,7649],{},[247,7648,1113],{},"-এর পরের প্রতিটা লাইন কার্যত একটা কলব্যাকের ভেতরে। সিরিয়ালি লেখা যায় বলে এটা ভুলে যাওয়া সহজ।",[367,7651,7653],{"id":7652},"রেন্ডারিং-কোথায়","রেন্ডারিং কোথায়",[39,7655,7656],{},"ব্রাউজার সাধারণত প্রতি ফ্রেমে একবার রেন্ডার করে — মোটামুটি ১৬.৭ms পরপর, ৬০Hz-এ। ধাপটা এরকম:",[241,7658,7661],{"className":7659,"code":7660,"language":2504},[2502],"ম্যাক্রোটাস্ক → মাইক্রোটাস্ক (সব) → rAF কলব্যাক → স্টাইল → লেআউট → পেইন্ট\n",[247,7662,7660],{"__ignoreMap":46},[39,7664,7665],{},"দুইটা ব্যবহারিক ফল:",[39,7667,7668,7671],{},[517,7669,7670],{},"এক"," — একই টিকে DOM বদলে আবার পড়লে ব্রাউজার জোর করে লেআউট হিসাব করে (layout thrashing):",[241,7673,7675],{"className":243,"code":7674,"language":245,"meta":46,"style":46},"\u002F\u002F খারাপ — প্রতি ধাপে জোর করে reflow\nfor (const el of items) {\n  el.style.height = el.offsetHeight + 10 + 'px'\n}\n\n\u002F\u002F ভালো — আগে সব পড়ুন, তারপর সব লিখুন\nconst heights = items.map((el) => el.offsetHeight)\nitems.forEach((el, i) => (el.style.height = heights[i] + 10 + 'px'))\n",[247,7676,7677,7682,7699,7719,7723,7727,7732,7760],{"__ignoreMap":46},[250,7678,7679],{"class":252,"line":56},[250,7680,7681],{"class":434},"\u002F\u002F খারাপ — প্রতি ধাপে জোর করে reflow\n",[250,7683,7684,7686,7688,7690,7693,7696],{"class":252,"line":203},[250,7685,6716],{"class":255},[250,7687,7319],{"class":263},[250,7689,386],{"class":255},[250,7691,7692],{"class":389}," el",[250,7694,7695],{"class":255}," of",[250,7697,7698],{"class":263}," items) {\n",[250,7700,7701,7704,7706,7709,7711,7713,7716],{"class":252,"line":47},[250,7702,7703],{"class":263},"  el.style.height ",[250,7705,614],{"class":255},[250,7707,7708],{"class":263}," el.offsetHeight ",[250,7710,2160],{"class":255},[250,7712,4906],{"class":389},[250,7714,7715],{"class":255}," +",[250,7717,7718],{"class":278}," 'px'\n",[250,7720,7721],{"class":252,"line":290},[250,7722,287],{"class":263},[250,7724,7725],{"class":252,"line":296},[250,7726,293],{"emptyLinePlaceholder":24},[250,7728,7729],{"class":252,"line":306},[250,7730,7731],{"class":434},"\u002F\u002F ভালো — আগে সব পড়ুন, তারপর সব লিখুন\n",[250,7733,7734,7736,7739,7741,7744,7747,7750,7753,7755,7757],{"class":252,"line":315},[250,7735,386],{"class":255},[250,7737,7738],{"class":389}," heights",[250,7740,393],{"class":255},[250,7742,7743],{"class":263}," items.",[250,7745,7746],{"class":259},"map",[250,7748,7749],{"class":263},"((",[250,7751,7752],{"class":709},"el",[250,7754,5217],{"class":263},[250,7756,990],{"class":255},[250,7758,7759],{"class":263}," el.offsetHeight)\n",[250,7761,7762,7765,7768,7770,7772,7774,7777,7779,7781,7784,7786,7789,7791,7793,7795,7798],{"class":252,"line":329},[250,7763,7764],{"class":263},"items.",[250,7766,7767],{"class":259},"forEach",[250,7769,7749],{"class":263},[250,7771,7752],{"class":709},[250,7773,1687],{"class":263},[250,7775,7776],{"class":709},"i",[250,7778,5217],{"class":263},[250,7780,990],{"class":255},[250,7782,7783],{"class":263}," (el.style.height ",[250,7785,614],{"class":255},[250,7787,7788],{"class":263}," heights[i] ",[250,7790,2160],{"class":255},[250,7792,4906],{"class":389},[250,7794,7715],{"class":255},[250,7796,7797],{"class":278}," 'px'",[250,7799,1911],{"class":263},[39,7801,7802,7805,7806,7808,7809,7811],{},[517,7803,7804],{},"দুই"," — অ্যানিমেশন ",[247,7807,956],{}," দিয়ে নয়, ",[247,7810,2700],{}," দিয়ে। rAF রেন্ডার ধাপের ঠিক আগে চলে, তাই ফ্রেম মিস হয় না।",[367,7813,7815],{"id":7814},"০-আসলে-০-নয়","০ আসলে ০ নয়",[39,7817,7818,7819,7821,7822,7825],{},"শেষ একটা খুঁটিনাটি: HTML স্পেক অনুযায়ী নেস্টেড ",[247,7820,956],{}," পাঁচ স্তরের পর সর্বনিম্ন ",[517,7823,7824],{},"৪ মিলিসেকেন্ড"," ক্ল্যাম্প হয়। ব্যাকগ্রাউন্ড ট্যাবে ব্রাউজার এটা আরও বাড়িয়ে ১০০০ms-ও করে দিতে পারে।",[39,7827,7828,7829,7831,7832,7834],{},"অর্থাৎ ",[247,7830,1968],{}," মানে \"যত দ্রুত সম্ভব\" নয়, মানে \"পরের টাস্ক হিসেবে, সুবিধামতো\"। এখনই দরকার হলে ",[247,7833,1800],{}," আছে — কিন্তু তাতে ব্রাউজার রেন্ডার করার সুযোগ পায় না, যেটা প্রায়ই আপনি চাইছিলেন।",[477,7836,7837],{},"html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}html pre.shiki code .siJwO, html code.shiki .siJwO{--shiki-default:#F69D50;--shiki-dark:#F69D50}",{"title":46,"searchDepth":47,"depth":47,"links":7839},[7840,7841,7842,7843,7844,7845],{"id":7281,"depth":203,"text":7282},{"id":7352,"depth":203,"text":7353},{"id":7435,"depth":203,"text":7436},{"id":7541,"depth":203,"text":7542},{"id":7652,"depth":203,"text":7653},{"id":7814,"depth":203,"text":7815},"\u002Fcovers\u002Fblog\u002Fbn\u002Fevent-loop-settimeout-zero.webp","2026-06-11","ম্যাক্রোটাস্ক, মাইক্রোটাস্ক আর রেন্ডারিং — ইভেন্ট লুপের ধাপগুলো জানলে অ্যাসিঙ্ক কোডের ক্রম আর অনুমান করতে হয় না।",{},"\u002Fblog\u002Fbn\u002Fevent-loop-settimeout-zero",{"title":7172,"description":7848},"blog\u002Fbn\u002Fevent-loop-settimeout-zero",[7854,33,68],"JavaScript","H5TYNXXsO4KpEMqYBcmOopomo78ZO1gTGVkxRro95nQ",{"id":7857,"title":7858,"author":7859,"body":7860,"category":92,"cover":8013,"coverAlt":7858,"date":8014,"description":8015,"draft":6,"extension":51,"featured":6,"lang":53,"meta":8016,"navigation":24,"path":8017,"readingTime":306,"seo":8018,"stem":8019,"tags":8020,"__hash__":8022},"blog\u002Fblog\u002Fbn\u002Ffreelance-rate-setting.md","ফ্রিল্যান্সিংয়ে রেট ঠিক করা — ঘণ্টার হিসাব থেকে বেরোনোর উপায়",{"name":4962,"role":4963},{"type":36,"value":7861,"toc":8005},[7862,7865,7868,7871,7875,7878,7881,7889,7892,7896,7899,7931,7934,7938,7941,7944,7949,7952,7956,7959,7965,7976,7979,7983,7986,7991,7994,7998],[39,7863,7864],{},"ঘণ্টায় দাম নেওয়ার একটা কাঠামোগত সমস্যা আছে, আর সেটা রেট কম না বেশি তার সাথে সম্পর্কিত নয়।",[39,7866,7867],{},"আপনি যত ভালো হবেন, একই কাজ তত দ্রুত করবেন। ঘণ্টায় বিল করলে দক্ষতা বাড়ার সরাসরি ফল হলো আয় কমা। পাঁচ বছরের অভিজ্ঞতা দিয়ে যে কাজটা তিন ঘণ্টায় করছেন, নতুন কেউ সেটা বারো ঘণ্টায় করবে — এবং চারগুণ বেশি পাবে।",[39,7869,7870],{},"এটা কোনো নৈতিক সমস্যা নয়, হিসাবের সমস্যা। আর সমাধানটা রেট বাড়ানো নয়, ইউনিট বদলানো।",[367,7872,7874],{"id":7873},"ঘণ্টা-নয়-ফল","ঘণ্টা নয়, ফল",[39,7876,7877],{},"ক্লায়েন্ট ঘণ্টা কিনছে না। সে কিনছে একটা কাজ হয়ে যাওয়া — একটা ল্যান্ডিং পেজ যেটা কনভার্ট করে, একটা ড্যাশবোর্ড যেটা তার টিম বুঝতে পারে।",[39,7879,7880],{},"তাই প্রস্তাবটা এভাবে লেখা:",[1761,7882,7883],{},[39,7884,7885,7888],{},[517,7886,7887],{},"ল্যান্ডিং পেজ ডিজাইন ও বিল্ড"," — ৪টি সেকশন, রেসপন্সিভ, দুই রাউন্ড রিভিশন। ডেলিভারি ১২ কর্মদিবস। ৳XX,XXX।",[39,7890,7891],{},"\"৪০ ঘণ্টা × ৳X\" নয়। প্রথমটায় ঝুঁকি আপনার, দ্বিতীয়টায় ঝুঁকি ক্লায়েন্টের — আর ক্লায়েন্ট ঝুঁকি নিতে চায় বলেই আপনাকে ডেকেছে।",[367,7893,7895],{"id":7894},"সংখ্যা-বলার-আগে-যে-প্রশ্নগুলো","সংখ্যা বলার আগে যে প্রশ্নগুলো",[39,7897,7898],{},"দাম বলার আগে অন্তত এই কয়টার উত্তর দরকার। উত্তরগুলো না জেনে দেওয়া প্রতিটা কোটই আসলে একটা অনুমান:",[2038,7900,7901,7907,7913,7919,7925],{},[514,7902,7903,7906],{},[517,7904,7905],{},"কে সিদ্ধান্ত নেবে?"," একজন, না কমিটি? অনুমোদনকারীর সংখ্যার সাথে রিভিশনের সংখ্যা প্রায় রৈখিকভাবে বাড়ে।",[514,7908,7909,7912],{},[517,7910,7911],{},"কনটেন্ট কে দেবে?"," \"আপনি লিখে দিন\" একটা আলাদা প্রজেক্ট, এবং সেটাই সবচেয়ে বেশি সময় খায়।",[514,7914,7915,7918],{},[517,7916,7917],{},"কীসের সাথে যুক্ত হবে?"," একটা বিদ্যমান কোডবেস, নাকি নতুন? পুরোনো কোডে ঢোকার সময়টা কেউ হিসাব করে না।",[514,7920,7921,7924],{},[517,7922,7923],{},"ডেডলাইনটা কার?"," কোনো ইভেন্ট বা লঞ্চের সাথে বাঁধা থাকলে বাফার বাড়াতে হবে।",[514,7926,7927,7930],{},[517,7928,7929],{},"\"শেষ\" মানে কী?"," ডিজাইন ফাইল, নাকি ডিপ্লয় করা সাইট? এই একটা প্রশ্নে প্রজেক্টের আকার দ্বিগুণ হতে পারে।",[39,7932,7933],{},"শেষ প্রশ্নটাই সবচেয়ে বেশি টাকা বাঁচায়।",[367,7935,7937],{"id":7936},"বাফারটা-লুকিয়ে-রাখবেন-না","বাফারটা লুকিয়ে রাখবেন না",[39,7939,7940],{},"সবাই বাফার রাখে। বেশিরভাগ মানুষ সেটা রেটের ভেতর লুকিয়ে রাখে, তারপর স্কোপ বাড়লে অস্বস্তিতে পড়ে।",[39,7942,7943],{},"ভালো উপায় হলো সীমাটা লিখে রাখা:",[1761,7945,7946],{},[39,7947,7948],{},"দুই রাউন্ড রিভিশন অন্তর্ভুক্ত। এর পরের প্রতিটি রাউন্ড ৳X,XXX।",[39,7950,7951],{},"এটা ক্লায়েন্টের বিরুদ্ধে নয় — এটা তাকে তথ্য দেয়। এখন সে জানে তৃতীয় রাউন্ডের দাম কত, আর সিদ্ধান্তটা তার হাতে। যেসব প্রজেক্ট তিক্ত হয়, সেগুলোর প্রায় সবগুলোতেই এই লাইনটা ছিল না।",[367,7953,7955],{"id":7954},"প্রথম-সংখ্যাটা-কীভাবে-বের-করবেন","প্রথম সংখ্যাটা কীভাবে বের করবেন",[39,7957,7958],{},"একটা কাজ করা যায়: বছরে যত টাকা দরকার, সেটা থেকে পিছিয়ে হিসাব করুন।",[241,7960,7963],{"className":7961,"code":7962,"language":2504},[2502],"বার্ষিক লক্ষ্য              ৳১২,০০,০০০\n+ খরচ (সফটওয়্যার, ডিভাইস)     ৳১,০০,০০০\n+ কর ও অনিশ্চয়তা (~২৫%)       ৳৩,২৫,০০০\n────────────────────────────────────────\nমোট দরকার                   ৳১৬,২৫,০০০\n\nবিলযোগ্য সপ্তাহ (ছুটি ও ফাঁকা বাদে)      ৪০\nসপ্তাহে বিলযোগ্য দিন (বাকিটা সেলস, শেখা)  ৩\n────────────────────────────────────────\nবিলযোগ্য দিন                 ১২০\nদৈনিক হার                    ~৳১৩,৫০০\n",[247,7964,7962],{"__ignoreMap":46},[39,7966,7967,7968,7971,7972,7975],{},"গুরুত্বপূর্ণ সংখ্যাটা ",[517,7969,7970],{},"৫২ সপ্তাহ নয়, ৪০"," এবং ",[517,7973,7974],{},"৫ দিন নয়, ৩","। প্রস্তাব লেখা, ইমেইল, ইনভয়েস, নতুন কিছু শেখা — এগুলো কাজের অংশ, কিন্তু কেউ এর বিল দেয় না। ৫ দিন ধরে হিসাব করলে আপনি সপ্তাহে সাত দিন কাজ করেও লক্ষ্যে পৌঁছাবেন না।",[39,7977,7978],{},"এই দৈনিক হারটা ক্লায়েন্টকে বলার জন্য নয়। এটা আপনার নিজের জন্য — একটা ফিক্সড প্রাইস দেওয়ার পর যাচাই করার মাপকাঠি।",[367,7980,7982],{"id":7981},"কম-রেটে-পোর্টফোলিওর-জন্য-কাজ","কম রেটে \"পোর্টফোলিওর জন্য\" কাজ",[39,7984,7985],{},"মাঝে মাঝে যুক্তিসঙ্গত — শুরুতে, বা এমন একটা কাজ যেটা সত্যিই দেখানোর মতো হবে। কিন্তু তখনও ছাড়টা লিখে রাখুন:",[1761,7987,7988],{},[39,7989,7990],{},"স্বাভাবিক মূল্য ৳XX,XXX। এই প্রজেক্টে ৳XX,XXX — কেস স্টাডি হিসেবে প্রকাশের অনুমতির বিনিময়ে।",[39,7992,7993],{},"কারণ ছাড়টা অদৃশ্য হলে সেটাই আপনার রেট হয়ে যায়। ওই ক্লায়েন্ট পরের প্রজেক্টে একই সংখ্যা আশা করবে, আর সে যাকে রেফার করবে সেও তাই করবে।",[367,7995,7997],{"id":7996},"শেষ-কথা","শেষ কথা",[39,7999,8000,8001,8004],{},"রেট নিয়ে সবচেয়ে কাজের পরিবর্তনটা আসলে সংখ্যায় নয় — সেটা হলো ",[517,8002,8003],{},"দাম বলার আগে প্রশ্ন করার অভ্যাস","। যে প্রজেক্টগুলোতে লোকসান হয়, সেগুলো কম রেটের কারণে হয় না; হয় এমন একটা স্কোপের কারণে যেটা কেউ শুরুতে লিখে রাখেনি।",{"title":46,"searchDepth":47,"depth":47,"links":8006},[8007,8008,8009,8010,8011,8012],{"id":7873,"depth":203,"text":7874},{"id":7894,"depth":203,"text":7895},{"id":7936,"depth":203,"text":7937},{"id":7954,"depth":203,"text":7955},{"id":7981,"depth":203,"text":7982},{"id":7996,"depth":203,"text":7997},"\u002Fcovers\u002Fblog\u002Fbn\u002Ffreelance-rate-setting.webp","2026-05-19","ঘণ্টায় দাম দিলে আপনি দ্রুত কাজ শেখার জন্য শাস্তি পান। স্কোপ, বাফার আর কীভাবে একটা সংখ্যা বলার আগে প্রশ্নগুলো করতে হয়।",{},"\u002Fblog\u002Fbn\u002Ffreelance-rate-setting",{"title":7858,"description":8015},"blog\u002Fbn\u002Ffreelance-rate-setting",[95,97,8021],"স্কোপ","kRP5oD821PrdPuxrcbDf0LGKTSr493YDSi8EjY4gaAA",{"id":8024,"title":8025,"author":8026,"body":8029,"category":3972,"cover":8173,"coverAlt":8025,"date":8174,"description":8175,"draft":6,"extension":51,"featured":24,"lang":110,"meta":8176,"navigation":24,"path":8177,"readingTime":329,"seo":8178,"stem":8179,"tags":8180,"__hash__":8184},"blog\u002Fblog\u002Fen\u002Fdesigning-for-models-that-are-wrong.md","Designing for models that are wrong",{"name":8027,"role":8028},"Kwame Boateng","Principal Product Designer",{"type":36,"value":8030,"toc":8163},[8031,8034,8037,8041,8044,8047,8051,8057,8060,8064,8067,8070,8090,8093,8097,8100,8107,8111,8114,8117,8137,8141,8147,8150,8153,8157,8160],[39,8032,8033],{},"A model with 94% accuracy is wrong roughly once every sixteen times. If your interface presents every output with identical confidence, users will meet that sixteenth case, and from then on they will discount all sixteen.",[39,8035,8036],{},"This is the central design problem in AI products, and it is not solved by making the model better. A 99% model is wrong once every hundred times, and a user who cannot tell which time it is still has to check all hundred.",[367,8038,8040],{"id":8039},"uncertainty-is-a-design-surface","Uncertainty is a design surface",[39,8042,8043],{},"The useful question is not \"how do we hide that the model is uncertain\" but \"how do we put uncertainty where the user can act on it.\"",[39,8045,8046],{},"A few patterns that hold up in practice.",[5146,8048,8050],{"id":8049},"show-the-evidence-next-to-the-conclusion","Show the evidence next to the conclusion",[39,8052,8053,8054,1886],{},"An answer alone is unverifiable. An answer with its sources is checkable in seconds. This is why citation-first interfaces outperform confident-summary interfaces even when the summaries are equally accurate — the user is not reading the citation, they are reading the ",[947,8055,8056],{},"fact that there is one",[39,8058,8059],{},"The cheap version of this is a link. The good version highlights the specific span of the source that produced the claim, so verification costs a glance rather than a page of reading.",[5146,8061,8063],{"id":8062},"bands-beat-percentages","Bands beat percentages",[39,8065,8066],{},"\"87% confident\" reads as false precision to most people, and as noise to the rest. In user testing we consistently see people round anything above 80% to \"yes\" and anything below to \"no,\" which discards the entire signal.",[39,8068,8069],{},"Calibrated bands work better because they carry a meaning rather than a number:",[2038,8071,8072,8078,8084],{},[514,8073,8074,8077],{},[517,8075,8076],{},"Strong"," — many similar cases in training, historically reliable here",[514,8079,8080,8083],{},[517,8081,8082],{},"Mixed"," — features disagree, worth a closer look",[514,8085,8086,8089],{},[517,8087,8088],{},"Sparse"," — little comparable data, treat as a prompt rather than an answer",[39,8091,8092],{},"The third band is the one teams resist shipping and the one users value most. Admitting the model is out of its depth is what makes the other two bands credible.",[5146,8094,8096],{"id":8095},"make-disagreement-cheap","Make disagreement cheap",[39,8098,8099],{},"If accepting a suggestion is one click and rejecting it is a modal with a required reason code, you have not built a feedback loop. You have built a compliance funnel, and the data coming out of it is worthless.",[39,8101,8102,8103,8106],{},"Symmetrical cost is the rule: rejecting should take exactly as long as accepting. Ask for a reason, make it optional, and accept free text. You will get ",[947,8104,8105],{},"more"," structured feedback this way, not less, because people who were previously avoiding the interaction entirely will now engage with it.",[367,8108,8110],{"id":8109},"latency-is-a-trust-signal","Latency is a trust signal",[39,8112,8113],{},"Streaming changed what users expect, but it also created a new failure mode: an interface that streams confidently and then contradicts itself three sentences in.",[39,8115,8116],{},"Some rules we apply:",[2038,8118,8119,8125,8131],{},[514,8120,8121,8124],{},[517,8122,8123],{},"Never stream a verdict you might retract."," Stream the reasoning, then commit to the conclusion. Reversing a rendered answer is worse than a slower answer.",[514,8126,8127,8130],{},[517,8128,8129],{},"Show the shape of the wait."," \"Searching 1,240 documents\" beats a spinner, not because it is faster but because it is falsifiable — the user can tell whether the system is doing something plausible.",[514,8132,8133,8136],{},[517,8134,8135],{},"Budget for the p95, not the median."," AI features have long tails. An interface that feels good at 800ms and broken at 6s will be remembered as broken.",[367,8138,8140],{"id":8139},"the-interaction-nobody-designs","The interaction nobody designs",[39,8142,8143,8144],{},"Almost every AI feature ships without a designed answer to: ",[947,8145,8146],{},"what happens after the user notices it was wrong?",[39,8148,8149],{},"Not the error state — the model returning something plausible and incorrect is not an error, it is normal operation. The question is whether the user has a path from \"this is wrong\" to \"the system knows it was wrong and I can move on.\"",[39,8151,8152],{},"Without that path, the user's only option is to stop using the feature. Which is what most of them do, quietly, and it does not show up as an error in any dashboard.",[367,8154,8156],{"id":8155},"what-this-costs","What this costs",[39,8158,8159],{},"Designing for uncertainty makes the happy path slightly worse. Citations take space. Bands are less satisfying than a big number. An easy reject button will be used.",[39,8161,8162],{},"That trade is worth making, because the alternative is not a cleaner interface — it is a feature users abandon after the first time it burns them. A product that is honest about being wrong 6% of the time gets used for the other 94%. A product that pretends to be certain gets used until the sixteenth case, and then not at all.",{"title":46,"searchDepth":47,"depth":47,"links":8164},[8165,8170,8171,8172],{"id":8039,"depth":203,"text":8040,"children":8166},[8167,8168,8169],{"id":8049,"depth":47,"text":8050},{"id":8062,"depth":47,"text":8063},{"id":8095,"depth":47,"text":8096},{"id":8109,"depth":203,"text":8110},{"id":8139,"depth":203,"text":8140},{"id":8155,"depth":203,"text":8156},"\u002Fcovers\u002Fblog\u002Fen\u002Fdesigning-for-models-that-are-wrong.webp","2026-05-02","Every AI feature has a failure rate. Most interfaces are designed as though it were zero, and users learn to distrust the whole product.",{},"\u002Fblog\u002Fen\u002Fdesigning-for-models-that-are-wrong",{"title":8025,"description":8175},"blog\u002Fen\u002Fdesigning-for-models-that-are-wrong",[8181,8182,8183],"AI","Product Design","Trust","MBTX59kM1xlP_eNQBxC5t-JS5qQhe3ZkMfkjV-vlp0U",{"id":8186,"title":8187,"author":8188,"body":8191,"category":8681,"cover":8682,"coverAlt":8187,"date":8683,"description":8684,"draft":6,"extension":51,"featured":6,"lang":110,"meta":8685,"navigation":24,"path":8686,"readingTime":306,"seo":8687,"stem":8688,"tags":8689,"__hash__":8693},"blog\u002Fblog\u002Fen\u002Fcomponent-api-design.md","Your component API is a contract, so write it down",{"name":8189,"role":8190},"Sofia Lindqvist","Staff Frontend Engineer",{"type":36,"value":8192,"toc":8672},[8193,8196,8207,8210,8214,8217,8346,8352,8355,8359,8370,8373,8438,8451,8455,8458,8471,8483,8487,8490,8551,8562,8565,8600,8604,8610,8623,8640,8644,8647,8658,8660,8663,8669],[39,8194,8195],{},"A component library fails in a predictable way. Not all at once — one prop at a time.",[39,8197,8198,8199,8202,8203,8206],{},"Someone needs a slightly different button. They add ",[247,8200,8201],{},"isCompact",". Someone else needs a different one and adds ",[247,8204,8205],{},"variant=\"small\"",". Six months later the component takes fourteen props, four of which interact in ways nobody has documented, and everyone has quietly gone back to writing their own.",[39,8208,8209],{},"The fix is to treat the component's visual API as a contract that is written down in one place and enforced.",[367,8211,8213],{"id":8212},"the-variant-table","The variant table",[39,8215,8216],{},"Every component should declare its full surface as data, not as branching logic scattered through the template.",[241,8218,8220],{"className":5026,"code":8219,"language":5028,"meta":46,"style":46},"const button = tv({\n  base: 'inline-flex items-center justify-center font-medium',\n  variants: {\n    variant: {\n      primary: 'bg-primary text-primary-foreground',\n      outline: 'border border-border-strong bg-transparent',\n      ghost: 'text-foreground-secondary hover:bg-surface-hover',\n    },\n    size: {\n      sm: 'h-9 px-3.5 text-sm',\n      md: 'h-11 px-5 text-sm',\n      lg: 'h-12 px-6 text-md',\n    },\n  },\n  defaultVariants: { variant: 'primary', size: 'md' },\n})\n",[247,8221,8222,8234,8243,8247,8251,8260,8270,8279,8283,8288,8298,8308,8318,8322,8326,8342],{"__ignoreMap":46},[250,8223,8224,8226,8228,8230,8232],{"class":252,"line":56},[250,8225,386],{"class":255},[250,8227,6459],{"class":389},[250,8229,393],{"class":255},[250,8231,6464],{"class":259},[250,8233,6467],{"class":263},[250,8235,8236,8238,8241],{"class":252,"line":203},[250,8237,6472],{"class":263},[250,8239,8240],{"class":278},"'inline-flex items-center justify-center font-medium'",[250,8242,5097],{"class":263},[250,8244,8245],{"class":252,"line":47},[250,8246,6482],{"class":263},[250,8248,8249],{"class":252,"line":290},[250,8250,6487],{"class":263},[250,8252,8253,8255,8258],{"class":252,"line":296},[250,8254,6492],{"class":263},[250,8256,8257],{"class":278},"'bg-primary text-primary-foreground'",[250,8259,5097],{"class":263},[250,8261,8262,8265,8268],{"class":252,"line":306},[250,8263,8264],{"class":263},"      outline: ",[250,8266,8267],{"class":278},"'border border-border-strong bg-transparent'",[250,8269,5097],{"class":263},[250,8271,8272,8274,8277],{"class":252,"line":315},[250,8273,6502],{"class":263},[250,8275,8276],{"class":278},"'text-foreground-secondary hover:bg-surface-hover'",[250,8278,5097],{"class":263},[250,8280,8281],{"class":252,"line":329},[250,8282,6512],{"class":263},[250,8284,8285],{"class":252,"line":334},[250,8286,8287],{"class":263},"    size: {\n",[250,8289,8290,8293,8296],{"class":252,"line":339},[250,8291,8292],{"class":263},"      sm: ",[250,8294,8295],{"class":278},"'h-9 px-3.5 text-sm'",[250,8297,5097],{"class":263},[250,8299,8300,8303,8306],{"class":252,"line":864},[250,8301,8302],{"class":263},"      md: ",[250,8304,8305],{"class":278},"'h-11 px-5 text-sm'",[250,8307,5097],{"class":263},[250,8309,8310,8313,8316],{"class":252,"line":1590},[250,8311,8312],{"class":263},"      lg: ",[250,8314,8315],{"class":278},"'h-12 px-6 text-md'",[250,8317,5097],{"class":263},[250,8319,8320],{"class":252,"line":1600},[250,8321,6512],{"class":263},[250,8323,8324],{"class":252,"line":1607},[250,8325,6533],{"class":263},[250,8327,8328,8331,8334,8337,8340],{"class":252,"line":83},[250,8329,8330],{"class":263},"  defaultVariants: { variant: ",[250,8332,8333],{"class":278},"'primary'",[250,8335,8336],{"class":263},", size: ",[250,8338,8339],{"class":278},"'md'",[250,8341,5081],{"class":263},[250,8343,8344],{"class":252,"line":2893},[250,8345,6538],{"class":263},[39,8347,8348,8349,8351],{},"The value here is not the terseness. It is that the table is exhaustive. If a variant is not in it, it does not exist, and a request for a new one is a visible decision rather than an inline ",[247,8350,5506],{}," override that nobody reviews.",[39,8353,8354],{},"This is the single highest-leverage thing you can do to stop a library from decaying.",[367,8356,8358],{"id":8357},"booleans-are-enums-that-have-not-grown-up-yet","Booleans are enums that have not grown up yet",[39,8360,8361,8363,8364,3680,8366,8369],{},[247,8362,8201],{}," seems fine until you need a third density. Then you get ",[247,8365,8201],{},[247,8367,8368],{},"isSpacious"," and the undefined behaviour when both are true.",[39,8371,8372],{},"If a property has any chance of gaining a third value, make it an enum on day one:",[241,8374,8376],{"className":5026,"code":8375,"language":5028,"meta":46,"style":46},"\u002F\u002F Will break\ndefineProps\u003C{ isCompact?: boolean }>()\n\n\u002F\u002F Will not\ndefineProps\u003C{ density?: 'compact' | 'default' | 'comfortable' }>()\n",[247,8377,8378,8383,8402,8406,8411],{"__ignoreMap":46},[250,8379,8380],{"class":252,"line":56},[250,8381,8382],{"class":434},"\u002F\u002F Will break\n",[250,8384,8385,8388,8391,8393,8396,8399],{"class":252,"line":203},[250,8386,8387],{"class":259},"defineProps",[250,8389,8390],{"class":263},"\u003C{ ",[250,8392,8201],{"class":709},[250,8394,8395],{"class":255},"?:",[250,8397,8398],{"class":389}," boolean",[250,8400,8401],{"class":263}," }>()\n",[250,8403,8404],{"class":252,"line":47},[250,8405,293],{"emptyLinePlaceholder":24},[250,8407,8408],{"class":252,"line":290},[250,8409,8410],{"class":434},"\u002F\u002F Will not\n",[250,8412,8413,8415,8417,8420,8422,8425,8428,8431,8433,8436],{"class":252,"line":296},[250,8414,8387],{"class":259},[250,8416,8390],{"class":263},[250,8418,8419],{"class":709},"density",[250,8421,8395],{"class":255},[250,8423,8424],{"class":278}," 'compact'",[250,8426,8427],{"class":255}," |",[250,8429,8430],{"class":278}," 'default'",[250,8432,8427],{"class":255},[250,8434,8435],{"class":278}," 'comfortable'",[250,8437,8401],{"class":263},[39,8439,8440,8441,1687,8444,1687,8447,8450],{},"Booleans are correct for genuinely binary states — ",[247,8442,8443],{},"disabled",[247,8445,8446],{},"loading",[247,8448,8449],{},"required",". Everything describing appearance is a candidate for growth.",[367,8452,8454],{"id":8453},"name-props-after-what-they-do","Name props after what they do",[39,8456,8457],{},"Two habits that pay for themselves:",[39,8459,8460,7010,8463,8466,8467,8470],{},[517,8461,8462],{},"Name the role, not the implementation.",[247,8464,8465],{},"trailingIcon"," survives a switch from an icon font to inline SVG. ",[247,8468,8469],{},"iconRightSvg"," does not.",[39,8472,8473,8476,8477,8479,8480,8482],{},[517,8474,8475],{},"Match the platform where one exists."," If the DOM calls it ",[247,8478,8443],{},", call it ",[247,8481,8443],{},". Every renamed standard property is a thing every consumer has to learn.",[367,8484,8486],{"id":8485},"slots-for-content-props-for-configuration","Slots for content, props for configuration",[39,8488,8489],{},"The rule that keeps components from growing forever: if it is content, it belongs in a slot; if it changes behaviour or appearance, it is a prop.",[241,8491,8494],{"className":8492,"code":8493,"language":3849,"meta":46,"style":46},"language-vue shiki shiki-themes github-dark-dimmed github-dark-dimmed","\u003CUiButton variant=\"primary\" size=\"lg\">\n  \u003Ctemplate #leading>\n    \u003CAvatar :src=\"user.avatar\" size=\"xs\" \u002F>\n  \u003C\u002Ftemplate>\n  Continue as {{ user.name }}\n\u003C\u002FUiButton>\n",[247,8495,8496,8522,8527,8532,8537,8542],{"__ignoreMap":46},[250,8497,8498,8500,8503,8506,8508,8511,8514,8516,8519],{"class":252,"line":56},[250,8499,423],{"class":263},[250,8501,8502],{"class":1173},"UiButton",[250,8504,8505],{"class":389}," variant",[250,8507,614],{"class":263},[250,8509,8510],{"class":278},"\"primary\"",[250,8512,8513],{"class":389}," size",[250,8515,614],{"class":263},[250,8517,8518],{"class":278},"\"lg\"",[250,8520,8521],{"class":263},">\n",[250,8523,8524],{"class":252,"line":203},[250,8525,8526],{"class":263},"  \u003Ctemplate #leading>\n",[250,8528,8529],{"class":252,"line":47},[250,8530,8531],{"class":263},"    \u003CAvatar :src=\"user.avatar\" size=\"xs\" \u002F>\n",[250,8533,8534],{"class":252,"line":290},[250,8535,8536],{"class":263},"  \u003C\u002Ftemplate>\n",[250,8538,8539],{"class":252,"line":296},[250,8540,8541],{"class":263},"  Continue as {{ user.name }}\n",[250,8543,8544,8547,8549],{"class":252,"line":306},[250,8545,8546],{"class":263},"\u003C\u002F",[250,8548,8502],{"class":1173},[250,8550,8521],{"class":263},[39,8552,8553,8554,8557,8558,8561],{},"Without the slot, someone eventually asks for an ",[247,8555,8556],{},"avatarSrc"," prop, and then an ",[247,8559,8560],{},"avatarSize"," prop, and the button is now responsible for rendering avatars.",[39,8563,8564],{},"Provide a default inside the slot so the common case stays one line:",[241,8566,8568],{"className":8492,"code":8567,"language":3849,"meta":46,"style":46},"\u003Cslot name=\"leading\">\n  \u003CIcon v-if=\"icon\" :name=\"icon\" \u002F>\n\u003C\u002Fslot>\n",[247,8569,8570,8587,8592],{"__ignoreMap":46},[250,8571,8572,8574,8577,8580,8582,8585],{"class":252,"line":56},[250,8573,423],{"class":263},[250,8575,8576],{"class":1173},"slot",[250,8578,8579],{"class":389}," name",[250,8581,614],{"class":263},[250,8583,8584],{"class":278},"\"leading\"",[250,8586,8521],{"class":263},[250,8588,8589],{"class":252,"line":203},[250,8590,8591],{"class":263},"  \u003CIcon v-if=\"icon\" :name=\"icon\" \u002F>\n",[250,8593,8594,8596,8598],{"class":252,"line":47},[250,8595,8546],{"class":263},[250,8597,8576],{"class":1173},[250,8599,8521],{"class":263},[367,8601,8603],{"id":8602},"let-consumers-override-but-make-it-lose-predictably","Let consumers override, but make it lose predictably",[39,8605,8606,8607,8609],{},"A component that cannot be adjusted gets forked. A component that can be adjusted arbitrarily stops being a system. The middle path is to accept a ",[247,8608,5506],{}," prop, apply it last, and resolve conflicts deterministically:",[241,8611,8613],{"className":5026,"code":8612,"language":5028,"meta":46,"style":46},"cx(base, variantClasses, props.class)\n",[247,8614,8615],{"__ignoreMap":46},[250,8616,8617,8620],{"class":252,"line":56},[250,8618,8619],{"class":259},"cx",[250,8621,8622],{"class":263},"(base, variantClasses, props.class)\n",[39,8624,8625,8626,3599,8629,8631,8632,8635,8636,8639],{},"With ",[247,8627,8628],{},"tailwind-merge",[247,8630,8619],{},", a consumer passing ",[247,8633,8634],{},"h-14"," beats the variant's ",[247,8637,8638],{},"h-11"," instead of both landing in the class list and the winner depending on Tailwind's internal ordering. Overrides become predictable, which means people use them for genuine one-offs rather than forking.",[367,8641,8643],{"id":8642},"accessibility-belongs-in-the-primitive","Accessibility belongs in the primitive",[39,8645,8646],{},"If focus rings, ARIA wiring and reduced-motion handling live in the consumer, they will be forgotten. Not sometimes — reliably, under deadline.",[39,8648,8649,8650,8653,8654,8657],{},"Put them in the primitive. A ",[247,8651,8652],{},"UiField"," that owns its own id, ",[247,8655,8656],{},"aria-describedby"," and error announcement means no form in the codebase can ship an undescribed input, because the wiring is not the consumer's job.",[367,8659,7129],{"id":7128},[39,8661,8662],{},"Ask someone who did not build the library to add a new page with it, and watch without helping.",[39,8664,8665,8666,8668],{},"Every time they open the component's source to work out what a prop does, that is a documentation failure. Every time they write a custom ",[247,8667,5506],{}," to get something the variant table should have offered, that is an API failure. Both are cheap to fix in week one and expensive in month nine.",[477,8670,8671],{},"html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}html pre.shiki code .siJwO, html code.shiki .siJwO{--shiki-default:#F69D50;--shiki-dark:#F69D50}html pre.shiki code .sza-u, html code.shiki .sza-u{--shiki-default:#8DDB8C;--shiki-dark:#8DDB8C}",{"title":46,"searchDepth":47,"depth":47,"links":8673},[8674,8675,8676,8677,8678,8679,8680],{"id":8212,"depth":203,"text":8213},{"id":8357,"depth":203,"text":8358},{"id":8453,"depth":203,"text":8454},{"id":8485,"depth":203,"text":8486},{"id":8602,"depth":203,"text":8603},{"id":8642,"depth":203,"text":8643},{"id":7128,"depth":203,"text":7129},"engineering","\u002Fcovers\u002Fblog\u002Fen\u002Fcomponent-api-design.webp","2026-04-09","Variant tables, prop naming and the boolean that should have been an enum. Notes from maintaining component libraries other people have to use.",{},"\u002Fblog\u002Fen\u002Fcomponent-api-design",{"title":8187,"description":8684},"blog\u002Fen\u002Fcomponent-api-design",[8690,8691,8692],"Vue","Components","API Design","UGHo0vBRPnye5ZBaateSnSRWcf_erhdcP4Ws9hcrWWM",{"id":8695,"title":8696,"author":8697,"body":8698,"category":3948,"cover":8898,"coverAlt":8696,"date":8899,"description":8900,"draft":6,"extension":51,"featured":6,"lang":110,"meta":8901,"navigation":24,"path":8902,"readingTime":306,"seo":8903,"stem":8904,"tags":8905,"__hash__":8909},"blog\u002Fblog\u002Fen\u002Fdense-tables-that-stay-readable.md","Dense tables that stay readable",{"name":8027,"role":8028},{"type":36,"value":8699,"toc":8889},[8700,8703,8710,8714,8717,8720,8740,8743,8747,8750,8787,8827,8830,8834,8837,8840,8844,8847,8850,8853,8856,8860,8863,8866,8870,8873,8876,8880,8883,8886],[39,8701,8702],{},"The measure of a data table is not whether it displays the data. It is whether people stop exporting it.",[39,8704,8705,8706,8709],{},"Every dense-data product we have worked on had the same tell: the most-clicked control was ",[517,8707,8708],{},"Export to CSV",". That button is a review. It means the table can show the data but cannot support the work, so people take it somewhere that can.",[367,8711,8713],{"id":8712},"row-height-is-the-whole-argument","Row height is the whole argument",[39,8715,8716],{},"Everything about table density comes down to one number, and it is contested for good reason. Operators want more rows on screen. Readability wants fewer.",[39,8718,8719],{},"What we have landed on after several of these:",[2038,8721,8722,8728,8734],{},[514,8723,8724,8727],{},[517,8725,8726],{},"36px default."," Fits a 14px label with enough vertical breathing room to scan a column without losing your place.",[514,8729,8730,8733],{},[517,8731,8732],{},"28px compact mode."," Available, not default. Power users find it; nobody is forced into it.",[514,8735,8736,8739],{},[517,8737,8738],{},"Never below 24px."," Below that, horizontal scanning breaks down — the eye loses the row on the way across.",[39,8741,8742],{},"The compact mode matters more than the exact default. Density preference correlates with how long someone has used the product, and a single fixed height is wrong for one group or the other.",[367,8744,8746],{"id":8745},"alignment-carries-meaning","Alignment carries meaning",[39,8748,8749],{},"Column alignment is not aesthetic. It is how the eye knows what kind of value it is looking at.",[2038,8751,8752,8758,8775,8781],{},[514,8753,8754,8757],{},[517,8755,8756],{},"Numerics right-aligned, always."," Digits line up by place value, and a column of right-aligned numbers can be compared without reading any of them.",[514,8759,8760,7010,8763,8766,8767,8770,8771,8774],{},[517,8761,8762],{},"Tabular figures, always.",[247,8764,8765],{},"font-variant-numeric: tabular-nums",". Without it, proportional digits make ",[247,8768,8769],{},"1,111"," narrower than ",[247,8772,8773],{},"8,888"," and the column stops lining up even when right-aligned.",[514,8776,8777,8780],{},[517,8778,8779],{},"Identifiers monospaced."," Order numbers, SKUs, hashes. Fixed width makes a transposed character visible.",[514,8782,8783,8786],{},[517,8784,8785],{},"Text left-aligned, never centred."," Centred text has no consistent starting edge, so every row costs a fresh visual search.",[241,8788,8790],{"className":4588,"code":8789,"language":4590,"meta":46,"style":46},".cell-numeric {\n  text-align: right;\n  font-variant-numeric: tabular-nums;\n}\n",[247,8791,8792,8799,8811,8823],{"__ignoreMap":46},[250,8793,8794,8797],{"class":252,"line":56},[250,8795,8796],{"class":389},".cell-numeric",[250,8798,1064],{"class":263},[250,8800,8801,8804,8806,8809],{"class":252,"line":203},[250,8802,8803],{"class":389},"  text-align",[250,8805,1177],{"class":263},[250,8807,8808],{"class":389},"right",[250,8810,4615],{"class":263},[250,8812,8813,8816,8818,8821],{"class":252,"line":47},[250,8814,8815],{"class":389},"  font-variant-numeric",[250,8817,1177],{"class":263},[250,8819,8820],{"class":389},"tabular-nums",[250,8822,4615],{"class":263},[250,8824,8825],{"class":252,"line":290},[250,8826,287],{"class":263},[39,8828,8829],{},"Two lines of CSS, and it is the single most noticeable improvement you can make to a data table.",[367,8831,8833],{"id":8832},"status-needs-a-glyph-and-a-word","Status needs a glyph and a word",[39,8835,8836],{},"Colour-only status fails for the eight percent of men with a colour vision deficiency, and it fails for everyone in a screenshot pasted into Slack at low contrast.",[39,8838,8839],{},"Pair a shape with a label. A filled dot plus \"Delayed\" survives greyscale, low contrast, and being described over a phone call. Colour then becomes reinforcement rather than the carrier.",[367,8841,8843],{"id":8842},"the-filter-model-decides-everything","The filter model decides everything",[39,8845,8846],{},"Most table redesigns fail here rather than in the visual layer.",[39,8848,8849],{},"A single-select filter bar handles \"show me late shipments.\" Real questions are compound: \"late shipments from Rotterdam, excluding the two carriers in a weather delay.\" If the UI cannot express that, users export.",[39,8851,8852],{},"Stackable conditions — field, operator, value, joined with and\u002For — are more work to build and are the difference between a table people use and a table people download.",[39,8854,8855],{},"The other half is latency. If filtering costs a server round-trip, people stop exploring and start planning each query in advance, which is a completely different and much worse mode of work. Client-side filtering over a loaded window gets you to sub-50ms, and below that threshold people start poking at the data. That shift is the point of the whole exercise.",[367,8857,8859],{"id":8858},"saved-views-are-the-retention-feature","Saved views are the retention feature",[39,8861,8862],{},"Every operator has four or five recurring questions. If the table forgets its state on refresh, they rebuild the same view every morning, and the product feels like a tool they are fighting.",[39,8864,8865],{},"Named, pinnable views fix that. The unplanned benefit, every time we have shipped this, is that teams build a shared set of standard views and use them to onboard new hires. The feature turns into documentation.",[367,8867,8869],{"id":8868},"virtualise-but-settle-density-first","Virtualise, but settle density first",[39,8871,8872],{},"Row virtualisation makes row count irrelevant, and you will need it above a few thousand rows.",[39,8874,8875],{},"Do it in the right order, though. Virtualisers want a known row height, so locking the rendering strategy before the density decision means re-validating every height choice against the virtualiser's assumptions. Decide the height, then virtualise.",[367,8877,8879],{"id":8878},"the-metric","The metric",[39,8881,8882],{},"Not time on page. Not rows viewed.",[39,8884,8885],{},"Count CSV exports per user per week, and watch it go down. That number is the honest measure of whether people trust the interface enough to work inside it.",[477,8887,8888],{},"html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":47,"depth":47,"links":8890},[8891,8892,8893,8894,8895,8896,8897],{"id":8712,"depth":203,"text":8713},{"id":8745,"depth":203,"text":8746},{"id":8832,"depth":203,"text":8833},{"id":8842,"depth":203,"text":8843},{"id":8858,"depth":203,"text":8859},{"id":8868,"depth":203,"text":8869},{"id":8878,"depth":203,"text":8879},"\u002Fcovers\u002Fblog\u002Fen\u002Fdense-tables-that-stay-readable.webp","2026-03-14","Row height, alignment, tabular figures and the filter model. What actually makes a 40,000-row table usable rather than merely displayable.",{},"\u002Fblog\u002Fen\u002Fdense-tables-that-stay-readable",{"title":8696,"description":8900},"blog\u002Fen\u002Fdense-tables-that-stay-readable",[8906,8907,8908],"Data Visualisation","Dashboards","Tables","5Ny3y4HIkpKmySi2ICH8ig3RzEm527WowyL5qT7c7vI",{"id":8911,"title":8912,"author":8913,"body":8914,"category":4364,"cover":9021,"coverAlt":8912,"date":9022,"description":9023,"draft":6,"extension":51,"featured":6,"lang":110,"meta":9024,"navigation":24,"path":9025,"readingTime":296,"seo":9026,"stem":9027,"tags":9028,"__hash__":9032},"blog\u002Fblog\u002Fen\u002Fscoping-a-project-you-can-finish.md","Scoping a project you can actually finish",{"name":6590,"role":6591},{"type":36,"value":8915,"toc":9013},[8916,8919,8926,8930,8933,8944,8947,8950,8954,8957,8960,8963,8966,8970,8973,8980,8984,8987,8990,8993,8997,9000,9003,9007,9010],[39,8917,8918],{},"Projects rarely fail loudly. They go a little long, then a little longer, and by the time anyone says the word \"overrun\" the cause is three months back.",[39,8920,8921,8922,8925],{},"In our experience the cause is almost always the same: nobody wrote down what ",[947,8923,8924],{},"done"," meant while it was still cheap to argue about.",[367,8927,8929],{"id":8928},"agree-on-numbers-before-opening-a-design-tool","Agree on numbers before opening a design tool",[39,8931,8932],{},"Not \"improve onboarding.\" Two or three numbers, with current values, agreed by whoever owns the budget:",[2038,8934,8935,8938,8941],{},[514,8936,8937],{},"Trial-to-paid conversion: 11.0% today",[514,8939,8940],{},"Median time to first reconciliation: 47 minutes today",[514,8942,8943],{},"Onboarding support tickets per 100 trials: 34 today",[39,8945,8946],{},"This does three things. It forces a real conversation about which outcome matters most, which is where scope actually gets decided. It gives you a way to say no later — a request that does not move one of the three numbers is a separate project. And it means the end of the engagement is a fact rather than an opinion.",[39,8948,8949],{},"The uncomfortable part is that some numbers will not move. Say so in the report. A studio that only ever reports wins is a studio nobody can calibrate.",[367,8951,8953],{"id":8952},"the-brief-is-a-hypothesis","The brief is a hypothesis",[39,8955,8956],{},"Clients hire us to solve a problem, and describe it as a feature. Those are not the same thing, and the gap is where the value is.",[39,8958,8959],{},"On one fintech engagement, the brief was \"redesign the onboarding checklist.\" We looked at the funnel first and found checklist completion at 68% — and that completing it barely predicted conversion. The checklist was not the problem. The nine administrative steps it was faithfully guiding people through were.",[39,8961,8962],{},"Pushing back on that in week one was uncomfortable. Discovering it in week seven would have been worse for everyone.",[39,8964,8965],{},"Budget two weeks to test the brief before committing to it. On maybe a third of projects, the brief changes.",[367,8967,8969],{"id":8968},"cut-scope-in-writing-early","Cut scope in writing, early",[39,8971,8972],{},"Scope gets cut on every project. The only variable is whether it happens deliberately in week two or silently in week nine.",[39,8974,8975,8976,8979],{},"We keep an explicit ",[517,8977,8978],{},"not doing"," list in the scope document, and review it at every phase boundary. Items move off it when there is a reason and someone signs. The value is not the list — it is that \"we are not doing the mobile app in this phase\" is a sentence somebody read and agreed to, rather than an assumption two parties held differently for three months.",[367,8981,8983],{"id":8982},"phases-with-real-exits","Phases with real exits",[39,8985,8986],{},"An engagement split into phases where the only exit is \"keep going\" is not phased, it is just long.",[39,8988,8989],{},"Each phase should end with something usable on its own and a genuine option to stop. Discovery ends with a synthesis document and a recommendation that has value even if nothing else follows. Design ends with a validated prototype the in-house team could build from. Build ends with shipped code.",[39,8991,8992],{},"Telling a client they can stop after phase one loses work occasionally. It also means the clients who continue are continuing because it is working, which is a much better basis for the next three months.",[367,8994,8996],{"id":8995},"weekly-review-not-milestone-review","Weekly review, not milestone review",[39,8998,8999],{},"Milestone reviews concentrate all feedback into a few high-stakes meetings, where a fundamental objection arrives after three weeks of work built on the thing being objected to.",[39,9001,9002],{},"Weekly reviews of work in progress — rough, unfinished, clearly labelled as such — surface disagreements while they cost a day instead of a sprint. The hard part is cultural: teams need to get used to seeing unfinished work without reading it as a status report.",[367,9004,9006],{"id":9005},"the-honest-estimate","The honest estimate",[39,9008,9009],{},"When someone asks whether we can hit a date, the useful answer is a range with the assumptions attached: \"Ten to fourteen weeks. Fourteen if the API work is not done by week six, because the dashboard depends on it.\"",[39,9011,9012],{},"That gives the client something to act on. \"Twelve weeks\" gives them a number to be disappointed by.",{"title":46,"searchDepth":47,"depth":47,"links":9014},[9015,9016,9017,9018,9019,9020],{"id":8928,"depth":203,"text":8929},{"id":8952,"depth":203,"text":8953},{"id":8968,"depth":203,"text":8969},{"id":8982,"depth":203,"text":8983},{"id":8995,"depth":203,"text":8996},{"id":9005,"depth":203,"text":9006},"\u002Fcovers\u002Fblog\u002Fen\u002Fscoping-a-project-you-can-finish.webp","2026-02-05","Most engagements go wrong in week two, not week nine. What we changed after enough of them went long.",{},"\u002Fblog\u002Fen\u002Fscoping-a-project-you-can-finish",{"title":8912,"description":9023},"blog\u002Fen\u002Fscoping-a-project-you-can-finish",[9029,9030,9031],"Process","Scoping","Client Work","g4dPYbIFemFd41Ap-LOFeNJKDJCSfcdpSsiGkxuO4IU",{"id":9034,"title":9035,"author":9036,"body":9037,"category":8681,"cover":9379,"coverAlt":9035,"date":9380,"description":9381,"draft":6,"extension":51,"featured":6,"lang":110,"meta":9382,"navigation":24,"path":9383,"readingTime":296,"seo":9384,"stem":9385,"tags":9386,"__hash__":9390},"blog\u002Fblog\u002Fen\u002Fperformance-budgets-in-ci.md","Performance budgets belong in CI, not in a retrospective",{"name":8189,"role":8190},{"type":36,"value":9038,"toc":9371},[9039,9042,9045,9049,9052,9055,9208,9211,9214,9218,9229,9232,9236,9239,9255,9268,9274,9280,9284,9287,9294,9298,9301,9355,9358,9362,9365,9368],[39,9040,9041],{},"Every team has run a Lighthouse audit, celebrated a 96, and watched it drift to 71 over the following quarter. The audit was not wrong. It was just a photograph of a moving thing.",[39,9043,9044],{},"Performance is not a score you achieve. It is a constraint you enforce, and the only place a constraint holds is in the pipeline.",[367,9046,9048],{"id":9047},"budget-the-bytes-not-the-score","Budget the bytes, not the score",[39,9050,9051],{},"Lighthouse scores are noisy — they move with machine load, network conditions and the phase of the moon. Failing a build on a composite score produces flaky CI and, within two weeks, a team that skips the check.",[39,9053,9054],{},"Budget the things that are deterministic:",[241,9056,9060],{"className":9057,"code":9058,"language":9059,"meta":46,"style":46},"language-json shiki shiki-themes github-dark-dimmed github-dark-dimmed","{\n  \"budgets\": [\n    {\n      \"path\": \"\u002F*\",\n      \"resourceSizes\": [\n        { \"resourceType\": \"script\", \"budget\": 180 },\n        { \"resourceType\": \"stylesheet\", \"budget\": 60 },\n        { \"resourceType\": \"font\", \"budget\": 120 },\n        { \"resourceType\": \"total\", \"budget\": 500 }\n      ]\n    }\n  ]\n}\n","json",[247,9061,9062,9067,9074,9078,9090,9097,9122,9144,9166,9189,9194,9199,9204],{"__ignoreMap":46},[250,9063,9064],{"class":252,"line":56},[250,9065,9066],{"class":263},"{\n",[250,9068,9069,9072],{"class":252,"line":203},[250,9070,9071],{"class":1173},"  \"budgets\"",[250,9073,5065],{"class":263},[250,9075,9076],{"class":252,"line":47},[250,9077,5086],{"class":263},[250,9079,9080,9083,9085,9088],{"class":252,"line":290},[250,9081,9082],{"class":1173},"      \"path\"",[250,9084,1177],{"class":263},[250,9086,9087],{"class":278},"\"\u002F*\"",[250,9089,5097],{"class":263},[250,9091,9092,9095],{"class":252,"line":296},[250,9093,9094],{"class":1173},"      \"resourceSizes\"",[250,9096,5065],{"class":263},[250,9098,9099,9102,9105,9107,9110,9112,9115,9117,9120],{"class":252,"line":306},[250,9100,9101],{"class":263},"        { ",[250,9103,9104],{"class":1173},"\"resourceType\"",[250,9106,1177],{"class":263},[250,9108,9109],{"class":278},"\"script\"",[250,9111,1687],{"class":263},[250,9113,9114],{"class":1173},"\"budget\"",[250,9116,1177],{"class":263},[250,9118,9119],{"class":389},"180",[250,9121,5081],{"class":263},[250,9123,9124,9126,9128,9130,9133,9135,9137,9139,9142],{"class":252,"line":315},[250,9125,9101],{"class":263},[250,9127,9104],{"class":1173},[250,9129,1177],{"class":263},[250,9131,9132],{"class":278},"\"stylesheet\"",[250,9134,1687],{"class":263},[250,9136,9114],{"class":1173},[250,9138,1177],{"class":263},[250,9140,9141],{"class":389},"60",[250,9143,5081],{"class":263},[250,9145,9146,9148,9150,9152,9155,9157,9159,9161,9164],{"class":252,"line":329},[250,9147,9101],{"class":263},[250,9149,9104],{"class":1173},[250,9151,1177],{"class":263},[250,9153,9154],{"class":278},"\"font\"",[250,9156,1687],{"class":263},[250,9158,9114],{"class":1173},[250,9160,1177],{"class":263},[250,9162,9163],{"class":389},"120",[250,9165,5081],{"class":263},[250,9167,9168,9170,9172,9174,9177,9179,9181,9183,9186],{"class":252,"line":334},[250,9169,9101],{"class":263},[250,9171,9104],{"class":1173},[250,9173,1177],{"class":263},[250,9175,9176],{"class":278},"\"total\"",[250,9178,1687],{"class":263},[250,9180,9114],{"class":1173},[250,9182,1177],{"class":263},[250,9184,9185],{"class":389},"500",[250,9187,9188],{"class":263}," }\n",[250,9190,9191],{"class":252,"line":339},[250,9192,9193],{"class":263},"      ]\n",[250,9195,9196],{"class":252,"line":864},[250,9197,9198],{"class":263},"    }\n",[250,9200,9201],{"class":252,"line":1590},[250,9202,9203],{"class":263},"  ]\n",[250,9205,9206],{"class":252,"line":1600},[250,9207,287],{"class":263},[39,9209,9210],{},"Bundle size is stable across runs. If the JavaScript budget goes from 178KB to 214KB, something specific happened in this pull request, and the diff is right there.",[39,9212,9213],{},"Keep the Lighthouse run, but report it rather than gate on it. Gate on bytes.",[367,9215,9217],{"id":9216},"attribute-the-regression-in-the-pr","Attribute the regression in the PR",[39,9219,9220,9221,9224,9225,9228],{},"A build that fails with \"bundle too large\" sends someone on a twenty-minute hunt. A build that fails with \"",[247,9222,9223],{},"+34KB"," — ",[247,9226,9227],{},"date-fns"," added to the client bundle\" is fixed in two minutes.",[39,9230,9231],{},"Emit a comparison against the base branch as a PR comment: per-chunk deltas, and the new dependencies responsible. The point is to make the cheapest moment to fix a regression also the most obvious one.",[367,9233,9235],{"id":9234},"the-four-things-that-actually-regress","The four things that actually regress",[39,9237,9238],{},"Across the front-ends we have built, budget failures cluster:",[39,9240,9241,9244,9245,7017,9248,9251,9252,9254],{},[517,9242,9243],{},"A date or utility library reaching the client."," Almost always importable server-side only, or replaceable with ",[247,9246,9247],{},"Intl",[247,9249,9250],{},"Intl.DateTimeFormat"," covers the majority of what teams reach for ",[247,9253,9227],{}," to do.",[39,9256,9257,7010,9260,9263,9264,9267],{},[517,9258,9259],{},"An icon set imported wholesale.",[247,9261,9262],{},"import * as Icons"," pulls in thousands. Bundle the icons you use explicitly — and remember that dynamically named icons (",[247,9265,9266],{},":name=\"item.icon\"",") are invisible to static scanners, so they need listing by hand.",[39,9269,9270,9273],{},[517,9271,9272],{},"Fonts."," Self-host, subset to the ranges you serve, preload only the weights used above the fold. Four weights of two families at full Latin coverage is 300KB nobody asked for.",[39,9275,9276,9279],{},[517,9277,9278],{},"Images without dimensions."," Not a byte problem, a layout-shift problem, and the one most likely to make a fast site feel slow. Width and height on every image, always.",[367,9281,9283],{"id":9282},"measure-the-field-not-the-lab","Measure the field, not the lab",[39,9285,9286],{},"Lab metrics tell you whether a change helped. Field metrics tell you whether your users are having a good time. They disagree more often than people expect, because your laptop is not a mid-range Android on a train.",[39,9288,9289,9290,9293],{},"Ship ",[247,9291,9292],{},"web-vitals",", send p75 to your analytics, and put LCP, INP and CLS on a dashboard someone looks at weekly. When lab and field diverge, the field is right.",[367,9295,9297],{"id":9296},"reasonable-starting-numbers","Reasonable starting numbers",[39,9299,9300],{},"For a content-led marketing site, these are achievable without heroics:",[1278,9302,9303,9313],{},[1281,9304,9305],{},[1284,9306,9307,9310],{},[1287,9308,9309],{},"Metric",[1287,9311,9312],{},"Target (p75, field)",[1297,9314,9315,9323,9331,9339,9347],{},[1284,9316,9317,9320],{},[1302,9318,9319],{},"LCP",[1302,9321,9322],{},"\u003C 1.5s",[1284,9324,9325,9328],{},[1302,9326,9327],{},"INP",[1302,9329,9330],{},"\u003C 150ms",[1284,9332,9333,9336],{},[1302,9334,9335],{},"CLS",[1302,9337,9338],{},"\u003C 0.05",[1284,9340,9341,9344],{},[1302,9342,9343],{},"JS transferred",[1302,9345,9346],{},"\u003C 180KB",[1284,9348,9349,9352],{},[1302,9350,9351],{},"Total transferred",[1302,9353,9354],{},"\u003C 500KB",[39,9356,9357],{},"An application will need looser numbers. Set them deliberately rather than inheriting them, and write down why.",[367,9359,9361],{"id":9360},"the-part-that-is-cultural","The part that is cultural",[39,9363,9364],{},"The technical setup takes an afternoon. Keeping it takes agreement that a budget failure blocks a merge like a failing test does.",[39,9366,9367],{},"Without that, the check becomes advisory, advisory becomes ignored, and you are back to running an audit every quarter and wondering where the score went.",[477,9369,9370],{},"html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .sza-u, html code.shiki .sza-u{--shiki-default:#8DDB8C;--shiki-dark:#8DDB8C}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":47,"depth":47,"links":9372},[9373,9374,9375,9376,9377,9378],{"id":9047,"depth":203,"text":9048},{"id":9216,"depth":203,"text":9217},{"id":9234,"depth":203,"text":9235},{"id":9282,"depth":203,"text":9283},{"id":9296,"depth":203,"text":9297},{"id":9360,"depth":203,"text":9361},"\u002Fcovers\u002Fblog\u002Fen\u002Fperformance-budgets-in-ci.webp","2026-01-22","A Lighthouse score is a snapshot. A budget that fails the build is a constraint. Here is the setup we ship with every front-end.",{},"\u002Fblog\u002Fen\u002Fperformance-budgets-in-ci",{"title":9035,"description":9381},"blog\u002Fen\u002Fperformance-budgets-in-ci",[9387,9388,9389],"Performance","CI","Nuxt","BFNUPuWxsvteSofWkZcCgqIh7D3iOEk1VX-50PcfS5s",[9392,9927,10424,10910,11393,11879],{"id":9393,"title":9394,"accent":6994,"accentLight":9395,"body":9396,"challenge":9485,"client":9500,"cover":9501,"coverAlt":9502,"decisions":9503,"description":9534,"excerpt":61,"extension":51,"featured":24,"features":9535,"flows":9558,"gallery":9598,"ia":9635,"industry":9688,"kind":9689,"meta":9690,"navigation":24,"order":56,"outcomes":9691,"path":9716,"platform":9717,"principles":9720,"projectType":9739,"research":9740,"seo":9818,"services":9819,"stack":9822,"stem":9828,"system":9829,"timeline":9924,"year":9925,"__hash__":9926},"work\u002Fwork\u002F1.northwind-design-system.md","One design system across four acquired products","#0093B8",{"type":36,"value":9397,"toc":9479},[9398,9402,9405,9408,9411,9415,9418,9421,9438,9441,9445,9448,9451,9454,9458,9461,9467,9473],[367,9399,9401],{"id":9400},"why-build-us-a-component-library-was-the-wrong-brief","Why \"build us a component library\" was the wrong brief",[39,9403,9404],{},"The brief this scenario opens with is a component library. It is the brief most\nconsolidation projects get, and taking it literally is how you end up with\nvariant twelve.",[39,9406,9407],{},"A library is a supply-side fix. It assumes the problem is that good components\ndo not exist, so it makes them exist. But the failure mode in a four-product\nestate is not scarcity — Northwind has plenty of buttons, and several of them\nare good. The failure is that finding the right one costs more than writing a\nnew one, and a library does nothing about that arithmetic. It adds a thirteenth\nplace to look.",[39,9409,9410],{},"So the thing we designed is not a library. It is three separate mechanisms that\nhappen to ship together: a token architecture that makes a rebrand cheap, a\ncomponent contract that makes drift expressible only through review, and a\nmigration plan that replaces the old code rather than sitting beside it. Remove\nany one of them and the other two decay within two quarters.",[367,9412,9414],{"id":9413},"the-three-layer-bet","The three-layer bet",[39,9416,9417],{},"The core structural bet is the semantic layer, and it is worth being honest that\nit is a bet.",[39,9419,9420],{},"The obvious architecture is two layers: a palette, and components that use it.\nIt is simpler, it is easier to read, and it works fine until the first time\nanyone needs a second theme. At that point every component holds a decision that\nshould have been held centrally, and there are 48 places to make the same edit.",[39,9422,9423,9424,9427,9428,1687,9431,9433,9434,9437],{},"The three-layer version costs a hop. ",[247,9425,9426],{},"Button"," asks for ",[247,9429,9430],{},"primary",[247,9432,9430],{},"\nresolves to ",[247,9435,9436],{},"cyan-500"," in dark and to a different hue entirely in light — not a\nlighter step of the same one, because the bright cyan that carries a near-black\nsurface measures under 2:1 against white and is unreadable as a 1px border. That\nsubstitution is invisible to every component, which is the point, and it is also\nthe thing that makes the light theme a token edit instead of a redesign.",[39,9439,9440],{},"What we gave up is legibility at the call site. You cannot read a component and\nknow what colour it renders. New contributors find this genuinely irritating,\nand the honest defence is only that the alternative is worse at the scale where\nit matters.",[367,9442,9444],{"id":9443},"migration-as-the-deliverable","Migration as the deliverable",[39,9446,9447],{},"The part of this plan most likely to be cut is the four paired migration weeks,\nso it is scoped in from week one rather than proposed as a follow-up.",[39,9449,9450],{},"The reasoning is arithmetic. Codemods handle the mechanical transformations —\nraw values to token references, spacing literals to scale steps — which is most\nof the volume and none of the judgement. What is left is roughly 30% of the\nwork, and every item in it needs a decision: this table is close enough to the\nsystem table to migrate, that one is genuinely different and needs a variant, a\nthird should not exist at all.",[39,9452,9453],{},"Those decisions are fast when someone who knows the system sits next to someone\nwho knows the product, and they are indefinitely slow when they sit in a backlog\nbelonging to a team with its own roadmap. A half-migrated estate is worse than\nan unmigrated one, because now there are two right answers.",[367,9455,9457],{"id":9456},"where-this-would-strain","Where this would strain",[39,9459,9460],{},"Three places, stated plainly because a case study with no failure modes is a\nbrochure.",[39,9462,9463,9466],{},[517,9464,9465],{},"The variant-request turnaround is load-bearing."," The contract only holds if\nadding a legitimately new variant is fast. At a two-day turnaround teams will\nuse it; at two weeks they will find the override that still works, and the\ntable stops being a contract. Nothing in the architecture enforces the SLA — it\nis a staffing commitment wearing a technical hat.",[39,9468,9469,9472],{},[517,9470,9471],{},"The adoption metric is gameable."," Wrapping a bespoke component in a system\ncontainer raises the number without improving anything. Reporting it as a trend\nrather than a target, and keeping it out of performance review, is the only\nmitigation, and it is a social one.",[39,9474,9475,9478],{},[517,9476,9477],{},"Four products may not actually want one system."," The scenario assumes enough\noverlap to justify shared components, and that assumption deserves testing\nbefore the investment. If the billing product's tables and the observability\nproduct's tables have genuinely different requirements, the honest answer is two\nsystems on shared tokens — a much smaller, much less impressive project that\nwould be the right one.",{"title":46,"searchDepth":47,"depth":47,"links":9480},[9481,9482,9483,9484],{"id":9400,"depth":203,"text":9401},{"id":9413,"depth":203,"text":9414},{"id":9443,"depth":203,"text":9444},{"id":9456,"depth":203,"text":9457},{"summary":9486,"points":9487},"Northwind Cloud is a scenario we built around the most common way enterprise design debt accumulates: not through carelessness, but through acquisition. Four products, four front-end codebases, four teams who each made locally reasonable decisions and never had a reason to agree. The cost only becomes visible the first time someone tries to ship something across all four.",[9488,9491,9494,9497],{"label":9489,"detail":9490},"Nobody made a bad decision","A 6px radius in billing and a 4px radius in observability are both defensible in isolation. There is no villain to point at, which is precisely why the problem persists — every individual choice survives review, and only the aggregate is indefensible.",{"label":9492,"detail":9493},"Writing a new component is faster than finding the right one","This is the mechanism that generates variant twelve. As long as search costs more than reimplementation, a component library is just a thirteenth place to look. Any fix that does not change this arithmetic fails no matter how good the components are.",{"label":9495,"detail":9496},"A shared navigation needs four implementations","The trigger for the scenario. Northwind wants one navigation bar across all four surfaces, and discovers that shipping it means writing it four times and maintaining it four times, forever.",{"label":9498,"detail":9499},"Consistency is not the business case","Leadership does not fund tidiness. The case has to be made in delivery time and in the cost of the next cross-product feature, which means the system has to be measurable from the first week.","Northwind Cloud","\u002Fcase-studies\u002Fnorthwind\u002Fscreens\u002Ftoken-architecture.webp","Northwind design system — the three-layer token architecture screen",[9504,9510,9516,9522,9528],{"title":9505,"problem":9506,"decision":9507,"rationale":9508,"tradeoff":9509},"A semantic layer between primitives and components","With components reading the palette directly, every theme, brand or contrast variation means touching all 48 of them. The alternative — one set of role names in the middle — costs the reader a hop.","Components may only reference semantic roles. The primitive ramps are private to the token build, and a lint rule fails any component that imports one.","It converts a 48-file change into a one-file change, and it makes the light theme possible at all: several roles resolve to a *different* hue in light mode, not a lighter step of the same one, because the dark-mode accents fail contrast on a white surface.","Reading a component no longer tells you what colour it is. Debugging goes through one extra indirection, and new contributors reliably find this annoying for their first fortnight.",{"title":9511,"problem":9512,"decision":9513,"rationale":9514,"tradeoff":9515},"The variant table is the component's contract","A component that accepts arbitrary class overrides has no API — it has a suggestion. That is how eleven buttons became eleven buttons, each one technically a customisation of the last.","Every component declares its variants, sizes and states in one table. Anything not in the table cannot be expressed, and `class` is merged through a conflict-aware resolver so intentional overrides still win predictably.","It moves the decision about whether a new style should exist from the moment of maximum deadline pressure to a review where someone can ask whether four products need it.","Genuinely one-off needs now require a system pull request. We budgeted a two-day turnaround on variant requests specifically so this does not become the reason teams route around the system.",{"title":9517,"problem":9518,"decision":9519,"rationale":9520,"tradeoff":9521},"Codemods for the mechanical 70%, pairing for the rest","Migration is where design systems die. A backlog of 2,800 replacements assigned to teams with their own roadmaps will not be finished, and a half-migrated estate is worse than an unmigrated one.","Ship codemods with the system for the mechanical transformations — raw values to token references, spacing literals to scale steps — and schedule one paired week per product team for the remainder.","The mechanical part is most of the volume and none of the judgement. The remaining 30% needs a decision per case, and a person from the system team sitting with the product team makes those decisions in minutes.","Four paired weeks is a real cost, and it is the line item most likely to be cut. We put it in the initial scope rather than the follow-up precisely because a follow-up budget does not survive a reprioritisation.",{"title":9523,"problem":9524,"decision":9525,"rationale":9526,"tradeoff":9527},"Adoption reported in CI, not in a survey","\"Are people using it?\" is the question that decides whether the system gets a second year of investment, and it is usually answered with anecdote.","Every pull request reports the share of rendered UI coming from system components, and the figure goes on a dashboard the engineering leads already look at.","It makes drift visible on the day it happens rather than at the next audit, and it gives the system team an argument that does not depend on anyone's goodwill.","Any measure that becomes a target gets gamed. A team can raise the number by wrapping a bespoke component in a system container, so the figure is reported as a trend and never used in performance review.",{"title":9529,"problem":9530,"decision":9531,"rationale":9532,"tradeoff":9533},"Documentation written before the component","In the first draft of this plan the docs site was the last milestone. For six weeks the only reference would have been Figma, which means engineers asking designers questions a page would have answered.","The documentation page for a component is written and reviewed before the component is built, and the page is what gets reviewed.","An API you cannot explain in three paragraphs is usually wrong. Writing first surfaced four components whose props only made sense to the person who designed them.","Some rework, because a few APIs changed after the docs were written. Less than we expected, and cheaper than the alternative.","A concept study in design-system consolidation — replacing eleven button implementations and 2,847 ad-hoc colour values with a three-layer token architecture and a 48-component library.",[9536,9539,9542,9546,9550,9554],{"title":9537,"detail":9538,"icon":109},"Three-layer token architecture","Primitives, semantic roles and component tokens, generated from one source into CSS custom properties, TypeScript constants and Figma variables.",{"title":9540,"detail":9541,"icon":171},"48-component library","Each component declares its variants, sizes, states and keyboard contract in a single table that also generates its documentation.",{"title":9543,"detail":9544,"icon":9545},"Migration codemods","Shipped with the system rather than after it, covering the mechanical transformations that make up roughly 70% of the replacement work.","lucide:git-branch",{"title":9547,"detail":9548,"icon":9549},"Adoption reporting in CI","Every pull request reports the share of rendered UI coming from system components, so drift is visible the day it appears.","lucide:trending-up",{"title":9551,"detail":9552,"icon":9553},"Generated Figma variables","The design library is built from the same token source as the code, so the inspector emits role names instead of raw hexes.","simple-icons:figma",{"title":9555,"detail":9556,"icon":9557},"Documentation site with live specimens","Components render as themselves on the page, so the docs cannot drift from the implementation the way a screenshot can.","lucide:book-open",[9559,9576,9588],{"title":9560,"note":9561,"before":9562,"after":9572},"Using a component on a new screen","The nine-step version is not a process anyone designed. It is what happens when discovery is unsupported, and it is the flow the system has to beat on cost rather than on virtue.",[9563,9564,9565,9566,9567,9568,9569,9570,9571],"Read spec","Grep local repo","Grep three other repos","Ask in Slack","Wait for reply","Open Figma to read values","Copy raw hex and spacing","Write a local variant","Fix review comments",[9573,9574,9575],"Search one index","Import component","Pass props from the variant table",{"title":9577,"note":9578,"before":9579,"after":9585},"Changing the brand colour","The before path is not hypothetical drudgery — it is the reason rebrands get deferred for years in estates like this one.",[9580,9581,9582,9583,9584],"Find every hex across four repos","Judge which are the brand colour","Replace by hand","Miss the ones inside SVGs","Regression-test four products",[9586,9587],"Edit the semantic layer","Rebuild",{"title":9589,"note":9590,"before":9591,"after":9594},"Adding a variant that does not exist yet","Deliberately not frictionless. The point is that the friction now produces a shared component instead of a private one.",[9592,9593],"Override with a class","Ship",[9595,9596,9597],"Open a system pull request","Add the variant to the table","Release, then consume",[9599,9606,9611,9616,9623,9628],{"title":9600,"caption":9601,"src":9501,"width":9602,"alt":9603,"frame":9604,"span":9605},"Token architecture","The three layers as one page, showing which layer a value lives in and which layers may reference it. The reference rule is the loudest element because it is the argument the page exists to settle.",1440,"Northwind design system token architecture, three layers side by side","browser","full",{"title":9607,"caption":9608,"src":9609,"width":9602,"alt":9610,"frame":9604},"Component index","Search with alias matching — a team looking for \"dropdown\" finds Select. Solving discovery is what changes the build-versus-find arithmetic.","\u002Fcase-studies\u002Fnorthwind\u002Fscreens\u002Fcomponent-index.webp","Searchable component index with adoption meters on every card",{"title":9612,"caption":9613,"src":9614,"width":9602,"alt":9615,"frame":9604},"Variant table","Every appearance, size and state of Button rendered live. If a combination is not on this page, it cannot be expressed in a product.","\u002Fcase-studies\u002Fnorthwind\u002Fscreens\u002Fvariant-table.webp","Button variant matrix showing five appearances across three sizes",{"title":9617,"caption":9618,"src":9619,"width":9620,"alt":9621,"frame":9622},"Adoption in CI","Per-product adoption against the previous attempt, updated on every pull request — and the note that it is reported as a trend, never used in performance review.","\u002Fcase-studies\u002Fnorthwind\u002Fcomponents\u002Fmigration-dashboard--chart.webp",702,"Adoption trend chart comparing this migration against the previous attempt","plain",{"title":9624,"caption":9625,"src":9626,"width":9602,"alt":9627,"frame":9604,"span":9605},"Migration dashboard","The trend line is the artifact that keeps the system funded past its first quarter, with the remaining call sites named per product.","\u002Fcase-studies\u002Fnorthwind\u002Fscreens\u002Fmigration-dashboard.webp","Migration dashboard with per-product adoption and open pull requests",{"title":9629,"caption":9630,"src":9631,"width":9632,"alt":9633,"frame":9634},"Documentation, on a phone","Written before the component was built. A reference that only works at 1440 is the kind of thing a design system team ships without noticing.","\u002Fcase-studies\u002Fnorthwind\u002Fmobile\u002Fdocs.webp",390,"Button documentation page on mobile with a live specimen","mobile",[9636,9649,9662,9675],{"label":9637,"detail":9638,"children":9639},"Layer one — primitives","Raw palette ramps, type scale, spacing units. No product meaning, changes almost never.",[9640,9643,9646],{"label":9641,"detail":9642},"Colour ramps","11 steps per hue, generated, never referenced by a component",{"label":9644,"detail":9645},"Type scale","Nine steps with paired line-height and tracking",{"label":9647,"detail":9648},"Space & radius","A single 4px base unit and a six-step radius ramp",{"label":9650,"detail":9651,"children":9652},"Layer two — semantic roles","The only layer a product reads. Flips wholesale between themes and brands.",[9653,9656,9659],{"label":9654,"detail":9655},"Surface roles","background, surface, surface-raised, overlay",{"label":9657,"detail":9658},"Content roles","foreground, secondary, muted, inverse",{"label":9660,"detail":9661},"Intent roles","primary, danger, warning, success — each with a paired foreground",{"label":9663,"detail":9664,"children":9665},"Layer three — components","48 components, each with a declared variant table and accessibility contract.",[9666,9669,9672],{"label":9667,"detail":9668},"Primitives","Button, Input, Select, Checkbox, Badge, Avatar",{"label":9670,"detail":9671},"Composites","Table, Dialog, Drawer, Combobox, DatePicker",{"label":9673,"detail":9674},"Patterns","Page shell, empty state, form layout, data grid",{"label":9676,"detail":9677,"children":9678},"Distribution","One published artifact consumed identically by design tooling and every front-end.",[9679,9682,9685],{"label":9680,"detail":9681},"Figma library","Variables generated from the same token source as the code",{"label":9683,"detail":9684},"npm package","Versioned, with codemods shipped alongside breaking changes",{"label":9686,"detail":9687},"Documentation site","Live specimens, props tables, and the do\u002Fdo-not for each component","SaaS","concept",{},[9692,9696,9700,9704,9708,9712],{"value":9693,"label":9694,"description":9695},"11 → 1","Button implementations","Eleven divergent buttons across four codebases, expressed as one component whose variant table is its contract.",{"value":9697,"label":9698,"description":9699},"214","Tokens across three layers","Primitives, semantic roles and component tokens — replacing the 2,847 unrelated raw values the scenario audit assumes.",{"value":9701,"label":9702,"description":9703},"48","Components specified","Each with its variants, states, keyboard behaviour and accessibility contract declared in a single table.",{"value":9705,"label":9706,"description":9707},"1","Layer a rebrand touches","Products consume semantic roles only. Changing the brand is an edit to layer two, not a search across four repositories.",{"value":9709,"label":9710,"description":9711},"3","Steps to use a system component","Down from nine in the modelled current-state flow — see the flow comparison below.",{"value":9713,"label":9714,"description":9715},"4 → 1","Sources of truth","One published library, consumed identically by design tooling and every front-end.","\u002Fwork\u002Fnorthwind-design-system",[9718,9719,9686],"Web application","Design tooling",[9721,9724,9727,9730,9733,9736],{"title":9722,"detail":9723},"Products consume roles, never raw values","A component asks for `surface` or `border-subtle`, never `#18181B`. This is the constraint that makes a rebrand a one-layer edit, and it is worth the indirection it costs a reader.",{"title":9725,"detail":9726},"If a variant is not in the table, it does not exist","Each component declares its full variant, size and state matrix in one place. Consumers cannot invent a thirteenth style with a class override, which is what stopped the drift the library alone would not have.",{"title":9728,"detail":9729},"Migration is the deliverable","A system that does not replace the old code is a thirteenth way to build a button. Codemods, a tracked backlog and paired migration weeks are in scope from week one, not scheduled afterwards.",{"title":9731,"detail":9732},"Documentation ships before the component","Written first, it forces the API to be explainable. Written last, it documents whatever got built.",{"title":9734,"detail":9735},"Adoption is measured, not assumed","CI reports the share of rendered UI coming from system components on every pull request. Without a number, \"adoption\" is a feeling that leadership correctly discounts.",{"title":9737,"detail":9738},"The system serves the products, not the reverse","A component that only exists because the system wanted symmetry is overhead. Every entry earns its place by replacing something real.","Design system & component library",{"summary":9741,"premises":9742,"personas":9761,"journey":9788},"We started from an audit rather than a workshop, because the argument for a design system is usually lost in the abstract and won on a spreadsheet. The figures below are the scenario's premises — we chose them to sit at the plausible end of what a four-product estate looks like after three acquisitions.",[9743,9746,9749,9752,9755,9758],{"label":9744,"detail":9745},"2,847 unique colour values","Roughly 60% within one perceptual step of another value already in use. Different hexes nobody can tell apart, each one a decision someone made under deadline.",{"label":9747,"detail":9748},"31 spacing scales","Not 31 values — 31 distinct scales, several of which disagree about whether the base unit is 4px or 5px.",{"label":9750,"detail":9751},"Eleven button implementations","Across four repositories, with divergent hover states, focus treatments and disabled semantics. Three of them are inaccessible by keyboard.",{"label":9753,"detail":9754},"The complaint is discoverability, not aesthetics","Engineers in this scenario do not say the UI looks inconsistent. They say they cannot find the component they need, so they write one.",{"label":9756,"detail":9757},"Design and engineering disagree on what \"done\" means","A designer considers a component done when the Figma variant set is complete. An engineer considers it done when it is merged and typed. Neither is wrong; the gap is where drift enters.",{"label":9759,"detail":9760},"There is no budget for a freeze","All four products keep shipping throughout. Any migration plan that requires a feature freeze is not a plan.",[9762,9775],{"name":9763,"role":9764,"context":9765,"goals":9766,"frustrations":9770,"quote":9774},"Dana Okonjo","Staff Frontend Engineer, Billing","Ships two or three features a sprint against a codebase she inherited from an acquired team. She is the person who would adopt the system, and the person who will route around it if it slows her down.",[9767,9768,9769],"Ship a feature without a three-day detour into component archaeology","Stop getting design-review comments about spacing she did not choose","Delete code rather than add it",[9771,9772,9773],"Four repositories to search, none with a shared naming convention","Components that exist but are undocumented, so their variants are unknown","Being told to \"use the design system\" when the system has no billing table","I am not writing another button because I want to. I am writing one because I have twenty minutes and I already spent fifteen looking.",{"name":9776,"role":9777,"context":9778,"goals":9779,"frustrations":9783,"quote":9787},"Ilse Brandt","Product Designer, Platform","Maintains the Figma library that three other designers draw from, and spends more of her week answering implementation questions than designing. She is the system's author and its bottleneck.",[9780,9781,9782],"Change a token once and see it land in all four products","Spend review time on interaction, not on radius values","Have a documented answer she can link to instead of retyping",[9784,9785,9786],"Every question a docs site would answer arrives as a direct message","Her library and the shipped code drift apart within a sprint","No way to see which components are actually being used","I have published the same explanation of our focus states eleven times. I have counted.",[9789,9793,9799,9804,9809,9814],{"stage":9790,"doing":9791,"feeling":9792},"Ticket picked up","Reads the spec, opens the Figma frame, identifies six UI elements needed.","neutral",{"stage":9794,"doing":9795,"feeling":9796,"friction":9797,"opportunity":9798},"Search for components","Greps the local repo, then the other three, then asks in Slack.","negative","No shared naming. A \"Select\" in one repo is a \"Dropdown\" in another.","One searchable index, with the same names in Figma and in code.",{"stage":9800,"doing":9801,"feeling":9796,"friction":9802,"opportunity":9803},"Decision point","Weighs adapting a found component against writing a fresh one.","Adapting means understanding someone else's variant logic. Writing is 20 minutes.","Make the found component obviously correct and obviously cheaper.",{"stage":9805,"doing":9806,"feeling":9792,"friction":9807,"opportunity":9808},"Build","Writes variant twelve, with hardcoded values copied from the Figma inspector.","The inspector gives raw hexes, so the new component enters the world untokenised.","Publish tokens to Figma so the inspector emits role names.",{"stage":9810,"doing":9811,"feeling":9796,"friction":9812,"opportunity":9813},"Design review","Receives comments on radius, hover colour and focus ring.","Rework on details that a system would have decided before she started.","Move these decisions out of review and into the component.",{"stage":9815,"doing":9816,"feeling":9792,"opportunity":9817},"Merged","Ships the feature. The estate now has twelve buttons.","Track component provenance in CI so the drift is visible as it happens.",{"title":9394,"description":9534},[9820,8182,9821],"Design Systems","Frontend Engineering",[9823,9824,9825,9826,9827],"Figma Variables","Vue 3","TypeScript","Tailwind CSS","Storybook","work\u002F1.northwind-design-system",{"summary":9830,"radius":306,"density":9831,"palette":9832,"type":9851,"tokens":9876,"components":9901,"specimens":9919},"Three token layers, 48 components and one distribution artifact. The specimens below are rendered live from this project's own token values rather than exported as images — a screenshot proves someone drew a button, a rendered specimen proves the button is generated by a system with parameters.","default",[9833,9837,9840,9843,9847],{"name":9834,"value":9835,"role":9836},"Ink","#0B0C0E","Primary surface in the dark theme",{"name":9838,"value":6994,"role":9839},"Signal","Primary action, dark theme only",{"name":9841,"value":9395,"role":9842},"Signal (light)","The same role restepped — the bright cyan fails contrast on white",{"name":9844,"value":9845,"role":9846},"Slate","#71717A","Secondary text and the reference series in charts",{"name":9848,"value":9849,"role":9850},"Amber","#F59E0B","Warning state, paired with an icon and never used alone",[9852,9858,9863,9868,9873],{"name":9853,"size":9854,"weight":9855,"tracking":9856,"usage":9857},"Display","2.5rem",600,"-0.03em","Page titles and empty-state headings",{"name":9859,"size":9860,"weight":9855,"tracking":9861,"usage":9862},"Title","1.5rem","-0.02em","Section and dialog headings",{"name":9864,"size":9865,"weight":9866,"usage":9867},"Body","0.9375rem",400,"Default running text and table cells",{"name":9869,"size":9870,"weight":9871,"usage":9872},"Label","0.8125rem",500,"Form labels, buttons and dense table headers",{"name":9874,"size":9870,"weight":9866,"usage":9875},"Mono","Identifiers, token names and code",[9877,9881,9885,9889,9893,9897],{"name":9878,"value":9879,"usage":9880},"--space-unit","4px","Every spacing step is a multiple. No exceptions, no half-steps.",{"name":9882,"value":9883,"usage":9884},"--radius-control","6px","Buttons, inputs and selects. Containers use 10px.",{"name":9886,"value":9887,"usage":9888},"--control-height","36px","The single height every interactive control aligns to.",{"name":9890,"value":9891,"usage":9892},"--border-hairline","1px","All dividers and container edges at every density.",{"name":9894,"value":9895,"usage":9896},"--duration-quick","180ms","State transitions. Anything longer feels broken on a dense screen.",{"name":9898,"value":9899,"usage":9900},"--focus-ring","2px solid","Offset by 2px, identical on every focusable element.",[9902,9904,9907,9910,9913,9916],{"name":9426,"variants":2938,"detail":9903},"Seven appearances × three sizes, plus loading, disabled and icon-only states.",{"name":9905,"variants":1590,"detail":9906},"Input","Three sizes × four validation states, each with a described error region.",{"name":9908,"variants":1590,"detail":9909},"Select","Native and combobox modes, with full keyboard navigation.",{"name":9911,"variants":329,"detail":9912},"Table","Three densities, sortable and selectable columns, virtualised body.",{"name":9914,"variants":306,"detail":9915},"Dialog","Focus trap, scroll lock and focus restore, in three widths.",{"name":9917,"variants":2893,"detail":9918},"Badge","Eight tones × two sizes. Status tones always carry an icon.",[9920,9921,9922,9923],"buttons","fields","status","nav","14-week scope","2026","9-m8Fo28vDEo5TPEhgGGnNAdAJLNpHjrgLXuK9bWkUU",{"id":9928,"title":9929,"accent":9930,"accentLight":9931,"body":9932,"challenge":10013,"client":10031,"cover":10032,"coverAlt":10033,"decisions":10034,"description":10065,"excerpt":61,"extension":51,"featured":6,"features":10066,"flows":10090,"gallery":10127,"ia":10159,"industry":10204,"kind":9689,"meta":10205,"navigation":24,"order":203,"outcomes":10206,"path":10229,"platform":10230,"principles":10233,"projectType":10252,"research":10253,"seo":10330,"services":10331,"stack":10334,"stem":10338,"system":10339,"timeline":10422,"year":9925,"__hash__":10423},"work\u002Fwork\u002F2.meridian-clinical-ai.md","Designing an AI that clinicians are allowed to disagree with","#22C55E","#15943F",{"type":36,"value":9933,"toc":10007},[9934,9938,9941,9944,9947,9951,9954,9957,9963,9966,9970,9973,9976,9979,9982,9985,9989,9995,10001],[367,9935,9937],{"id":9936},"the-problem-is-not-accuracy","The problem is not accuracy",[39,9939,9940],{},"The scenario deliberately gives Meridian a good model. That is the only way to\nsee the interface problem clearly.",[39,9942,9943],{},"If a recommendation is often wrong, the design question is uninteresting: fix\nthe model, and in the meantime make it easy to ignore. The hard and much more\ncommon case is a model that is usually right and still does not change what\nanyone does. At that point the failure is entirely in the space between the\nmodel's output and a professional's ability to act on it — which is the\ninterface, and nothing else.",[39,9945,9946],{},"What a clinician needs from a recommendation is not a conclusion. It is material\nfor a justification they will have to give later, possibly under scrutiny, from\nmemory, about a case they saw for four minutes eighteen months ago. A severity\nscore cannot enter that justification. It is not that clinicians distrust the\nnumber; it is that the number is not the kind of thing that can be cited.",[367,9948,9950],{"id":9949},"reversing-the-reading-order","Reversing the reading order",[39,9952,9953],{},"Almost everything in the design follows from one inversion: lead with the\nevidence, let the conclusion follow.",[39,9955,9956],{},"The current state shows a recommendation and hides the reasoning behind a\nnavigation. That ordering casts the clinician as a reviewer of the model's\noutput — their job is to accept or reject something already decided. Showing\nthe weighted record fields first casts them as the decision-maker, with the\nmodel as a colleague who has read the chart and has a view.",[39,9958,9959,9960,9962],{},"The second framing is both more accurate and safer. It is more accurate because\nthe clinician ",[947,9961,648],{}," the decision-maker, legally and clinically. It is safer\nbecause reasoning that is visible can be checked, and a conclusion that arrives\nalone can only be trusted or not.",[39,9964,9965],{},"This is also why the evidence panel is expanded by default rather than behind a\ndisclosure. A panel that must be opened is a panel that goes unopened on the\nshift where it would have mattered most.",[367,9967,9969],{"id":9968},"the-step-we-added-on-purpose","The step we added on purpose",[39,9971,9972],{},"Every other decision here removes work. One adds it, and it is the one worth\ndefending.",[39,9974,9975],{},"The initial impression — a brief recorded read before the model's output is\nrevealed — costs a step on every case. On a full queue that is real time, and it\nis the first thing that will be proposed for removal when throughput is under\npressure.",[39,9977,9978],{},"The argument for keeping it is that without it, the human in the loop is\ndecorative. Anchoring is not a bias people can notice in themselves and correct\nfor; the whole mechanism is that the anchored judgement feels like an\nindependent one. If the clinician sees the model's answer before forming a view,\ntheir agreement carries almost no information, and the safety case built on\n\"a clinician reviewed every recommendation\" is hollow.",[39,9980,9981],{},"So the step buys two things: an independent read, and a dataset of human\nassessments formed without model influence — which is, incidentally, the only\nhonest way to measure whether the model is helping.",[39,9983,9984],{},"We wrote that reasoning into the interface itself rather than leaving it in a\ndesign document, because the person who eventually proposes removing the step\nwill not read the design document.",[367,9986,9988],{"id":9987},"what-we-are-least-sure-about","What we are least sure about",[39,9990,9991,9994],{},[517,9992,9993],{},"Three bands may be too few."," Collapsing a continuous confidence into three\nstates throws away real information, and a clinician who was reading the\ngradient correctly is worse served. The defence is that most were not, but that\nis an assumption in this scenario, not a measurement. With real calibration data\nthe right answer might be four bands, or bands that vary by pathway.",[39,9996,9997,10000],{},[517,9998,9999],{},"Free text will be hard to live with."," It is the honest choice and it is\ngenuinely worse for anyone who needs to count things. Clustering approximates\nwhat a taxonomy would have given exactly, and approximate analysis of true\nreasons beats exact counts of false ones — but the safety lead will feel that\ntrade every quarter.",[39,10002,10003,10006],{},[517,10004,10005],{},"Symmetric actions have a real cost."," Removing the default removes the\nkeyboard fast-path, and on routine cases experienced clinicians will find the\nnew surface slower than the old one. That is the correct trade for a safety\nproperty, and it is still a cost that someone pays on every shift.",{"title":46,"searchDepth":47,"depth":47,"links":10008},[10009,10010,10011,10012],{"id":9936,"depth":203,"text":9937},{"id":9949,"depth":203,"text":9950},{"id":9968,"depth":203,"text":9969},{"id":9987,"depth":203,"text":9988},{"summary":10014,"points":10015},"The scenario starts from the uncomfortable finding that keeps recurring in deployed clinical AI: a model can be accurate and still be ignored. Meridian has a triage model with strong validated performance and a review interface that clinicians route around. The interesting question is not how to make the model better. It is what an interface owes a professional who will have to defend the decision to a colleague, a regulator, or a coroner.",[10016,10019,10022,10025,10028],{"label":10017,"detail":10018},"Accuracy is not the same as usability","A recommendation that is right 94% of the time is still unusable if a clinician cannot tell which case they are in. Aggregate accuracy is a property of the model; per-case confidence is a property of the interface, and only the second one helps at the bedside.",{"label":10020,"detail":10021},"An unexplained recommendation is not evidence","Clinicians are trained and required to justify decisions. A severity score with no indication of what in the record produced it cannot enter that justification, whatever its track record.",{"label":10023,"detail":10024},"The interface made the safe action expensive","In the current state, accepting is one click and disagreeing costs a modal, a taxonomy that does not fit real cases, and a free-text box. That asymmetry is a design decision, and it pushes toward automation bias every hour of every shift.",{"label":10026,"detail":10027},"Precision theatre erodes trust","Rendering confidence as \"87%\" implies a calibration the model does not have. Clinicians who notice this stop believing the number; clinicians who do not notice are misled by it. There is no audience it serves.",{"label":10029,"detail":10030},"Every interaction is an audit record","Whatever the interface captures is what exists in two years when someone asks why a decision was made. Designing the capture is designing the legal record.","Meridian Health","\u002Fcase-studies\u002Fmeridian\u002Fscreens\u002Freview-surface.webp","Meridian clinical review surface with evidence above the recommendation",[10035,10041,10047,10053,10059],{"title":10036,"problem":10037,"decision":10038,"rationale":10039,"tradeoff":10040},"Three calibrated bands instead of a confidence percentage","\"87% confident\" implies a precision the model cannot support and is read categorically anyway. Both the readers who trust it and the readers who discount it end up worse off than with no number.","Confidence renders as one of three bands — strong signal, mixed signal, weak signal — each with a fixed, written definition of what the model does and does not have in that state.","Three bands can be calibrated honestly and learned once. They also force a genuinely useful distinction the percentage hid: \"weak signal\" means the record is missing something, which is directly actionable in a way that \"62%\" is not.","Real information is discarded. A clinician who did read the gradient correctly loses resolution, and edge cases near a band boundary get rounded. We judged the loss smaller than the harm of false precision, but it is a genuine loss and it should be revisited with calibration data.",{"title":10042,"problem":10043,"decision":10044,"rationale":10045,"tradeoff":10046},"Evidence panel open by default","Reasoning behind a disclosure is reasoning nobody reads under time pressure, and the whole argument for this redesign is that an unjustifiable recommendation is unusable.","The record fields the model weighted are shown expanded, above the recommendation, each linked to the source it came from.","It makes the recommendation checkable in the few seconds a clinician actually has, and it changes the reading order from \"here is a conclusion, do you accept it\" to \"here is what I looked at, here is what I conclude\".","Significant vertical space on a screen that is already dense, and for experienced users on routine cases it is noise. A collapse control exists, but it does not persist between cases — we chose the cost of re-collapsing over the risk of a permanently hidden evidence panel.",{"title":10048,"problem":10049,"decision":10050,"rationale":10051,"tradeoff":10052},"Withhold the recommendation until an initial impression is recorded","Showing the model's answer first anchors the clinician's own assessment. Anchoring is invisible to the person it happens to, and it silently removes the independent judgement that justifies having a human reviewer.","The clinician records a brief initial impression, then the evidence and recommendation are revealed together.","It preserves an independent read, and as a side effect it produces exactly the dataset the safety lead needs — every case now carries a human assessment formed without model influence.","This is the most expensive decision on the page. It adds a step to every case, and on a shift with a full queue that is real time. It is also the decision most likely to be removed under throughput pressure, which is why the rationale is written into the interface rather than left in a design document.",{"title":10054,"problem":10055,"decision":10056,"rationale":10057,"tradeoff":10058},"Equal affordance for agree and disagree","When accepting is one click and rejecting is six, the interface has an opinion, and it applies that opinion at scale. Automation bias does not need much help.","Both actions are the same size, the same weight, the same distance from the pointer, and neither is a default. Neither is styled as primary.","Removing the asymmetry removes a systematic bias from thousands of decisions a week. It also improves the data: an override is now cheap enough to record honestly rather than skipped.","No default action means no keyboard fast-path for the common case, and experienced users on routine cases will find it slower. We accepted that cost; a default here is a thumb on the scale by construction.",{"title":10060,"problem":10061,"decision":10062,"rationale":10063,"tradeoff":10064},"Free text instead of an override taxonomy","A fixed reason list is faster to analyse and almost never fits the case in front of the clinician, so it collects whichever option is least wrong. That is worse than no data, because it looks like data.","One free-text field, with the structured fields — which evidence was shown, which band, what the initial impression was — captured automatically.","The structure comes from state the interface already has, so it costs the clinician nothing and is accurate. The part only a human can supply is left as prose, where it is honest.","Free text is expensive to analyse in aggregate and cannot be counted directly. The divergence view groups it with clustering, which is approximate and needs review — we preferred approximate analysis of true reasons over exact counts of false ones.","A concept study in clinical AI interface design — rebuilding a triage review surface around provenance, calibrated uncertainty and a disagreement path that costs no more than agreement.",[10067,10071,10075,10079,10083,10087],{"title":10068,"detail":10069,"icon":10070},"Calibrated confidence bands","Three states with fixed written definitions, replacing a percentage that was read categorically anyway.","lucide:gauge",{"title":10072,"detail":10073,"icon":10074},"Provenance-first review surface","The record fields the model weighted, linked to source, shown above the recommendation and expanded by default.","lucide:scan-eye",{"title":10076,"detail":10077,"icon":10078},"Anchor-resistant reveal","The recommendation is withheld until the clinician records an independent initial impression.","lucide:eye",{"title":10080,"detail":10081,"icon":10082},"Symmetric decision bar","Agree and disagree at identical weight, with no default and no primary styling on either.","lucide:git-compare",{"title":10084,"detail":10085,"icon":10086},"Zero-cost audit capture","Structured audit fields are taken from interface state, so a complete record is produced without the clinician doing anything extra.","lucide:file-text",{"title":10088,"detail":10089,"icon":141},"Divergence analysis","Overrides clustered by reason, surfacing where the model and clinical judgement systematically part company.",[10091,10104,10115],{"title":10092,"note":10093,"before":10094,"after":10101},"Recording a disagreement","This is the flow the whole project turns on. Every interaction removed here makes the safest action cheaper, thousands of times a week.",[10095,10096,10097,10098,10099,10100],"Click reject","Open modal","Choose a taxonomy reason","Type a justification","Confirm","Dismiss modal",[10102,10103],"Click disagree","Type the reason inline",{"title":10105,"note":10106,"before":10107,"after":10113},"Understanding why the model said this","In the current state the reasoning is not in the product. Moving it in is most of the work.",[10108,10109,10110,10111,10112],"Read the score","Open the model information page","Read the model card","Return to the case","Reconstruct which fields might apply",[10114],"Read the evidence panel, open by default",{"title":10116,"note":10117,"before":10118,"after":10123},"Working a case end to end","The redesigned flow is one step longer, and deliberately so — the initial impression is the step that stops the model anchoring the clinician.",[10119,10120,10121,10122],"Open case","See recommendation","Read record","Accept",[10119,10121,10124,10125,10126],"Record initial impression","Reveal evidence and recommendation","Agree or disagree",[10128,10132,10138,10144,10149,10154],{"title":10129,"caption":10130,"src":10032,"width":9602,"alt":10131,"frame":9604,"span":9605},"Review surface","Record, evidence, recommendation and both actions on one screen. Reading order runs evidence first, conclusion second — the inverse of the current state, and the inversion the whole redesign turns on.","Clinical review screen with patient record, weighted evidence and recommendation",{"title":10133,"caption":10134,"src":10135,"width":10136,"alt":10137,"frame":9622},"Equal affordance","Agree and disagree at identical size, weight and distance from the pointer. Neither is styled as primary, because a default here is a thumb on the scale applied thousands of times a week.","\u002Fcase-studies\u002Fmeridian\u002Fcomponents\u002Freview-surface--decision.webp",604,"Two identically weighted buttons, agree and disagree",{"title":10139,"caption":10140,"src":10141,"width":10142,"alt":10143,"frame":9622,"span":9605},"Calibrated bands","Three states with written definitions, replacing a percentage that was read categorically anyway. \"Weak signal\" names the absent field rather than emitting a low number, which is what makes it actionable.","\u002Fcase-studies\u002Fmeridian\u002Fcomponents\u002Fevidence-panel--bands.webp",1164,"Three confidence bands with written definitions and recommended actions",{"title":10145,"caption":10146,"src":10147,"width":9602,"alt":10148,"frame":9604},"Provenance","Each weighted field links to the record entry it came from, and the absent one is shown as a gap in the record rather than a negative finding.","\u002Fcase-studies\u002Fmeridian\u002Fscreens\u002Fevidence-panel.webp","Field weighting panel with links to source records",{"title":10150,"caption":10151,"src":10152,"width":9602,"alt":10153,"frame":9604},"Disagreement, inline","One control and a free-text box. The structured audit fields beside it are taken from interface state, so the clinician types only the part a person has to supply.","\u002Fcase-studies\u002Fmeridian\u002Fscreens\u002Fdisagreement.webp","Inline disagreement with automatically captured audit fields",{"title":10155,"caption":10156,"src":10157,"width":9632,"alt":10158,"frame":9634},"Initial impression","The step added on purpose, and the one most likely to be cut under throughput pressure — so its reasoning is written into the interface rather than left in a design document.","\u002Fcase-studies\u002Fmeridian\u002Fmobile\u002Finitial-impression.webp","Bedside tablet screen capturing the clinician's read before the model's",[10160,10170,10184,10194],{"label":10161,"detail":10162,"children":10163},"Case queue","The clinician's working list, ordered by their own triage rules rather than by model confidence.",[10164,10167],{"label":10165,"detail":10166},"Queue filters","By acuity, wait time and location — never by what the model thinks",{"label":10168,"detail":10169},"Case card","Shows presenting complaint only; no model output at list level",{"label":10129,"detail":10171,"children":10172},"One screen holding the record, the evidence, the recommendation and both actions.",[10173,10176,10178,10181],{"label":10174,"detail":10175},"Record summary","History, observations, medications — the clinician's own read",{"label":10155,"detail":10177},"Captured before the recommendation is revealed",{"label":10179,"detail":10180},"Evidence panel","The record fields the model weighted, each linked to its source",{"label":10182,"detail":10183},"Recommendation","Pathway, calibrated confidence band, and the caveat that applies",{"label":10185,"detail":10186,"children":10187},"Decision","Two equally weighted actions, plus the free-text reason that feeds the audit record.",[10188,10191],{"label":10189,"detail":10190},"Agree","Records agreement and the evidence shown at the time",{"label":10192,"detail":10193},"Disagree","One control, free-text reason, no taxonomy",{"label":10195,"detail":10196,"children":10197},"Audit trail","For the safety lead rather than the clinician — reconstructs any decision from stored state.",[10198,10201],{"label":10199,"detail":10200},"Decision replay","Exactly what was on screen when the decision was made",{"label":10202,"detail":10203},"Divergence view","Where clinicians systematically override, grouped by reason","Healthcare",{},[10207,10210,10214,10218,10222,10226],{"value":9713,"label":10208,"description":10209},"Interactions to record disagreement","A modal, a taxonomy select and a free-text justification collapsed into one inline control that captures the same audit data.",{"value":10211,"label":10212,"description":10213},"100%","Recommendations carrying provenance","Every recommendation links the specific record fields that drove it. Previously none did.",{"value":10215,"label":10216,"description":10217},"3 bands","Replacing a confidence percentage","A calibrated three-band scale, because \"87% confident\" was read as either meaningless or falsely precise.",{"value":10219,"label":10220,"description":10221},"9 → 4","Screens in the review path","Reasoning, evidence and history moved into the review surface instead of sitting behind separate navigations.",{"value":10223,"label":10224,"description":10225},"2","Actions with equal affordance","Accept and disagree are the same size, the same distance from the pointer, and neither is the default.",{"value":1006,"label":10227,"description":10228},"Automatic actions","The model never writes to the record. It argues; a person decides.","\u002Fwork\u002Fmeridian-clinical-ai",[10231,10232],"Clinical web application","Tablet, bedside",[10234,10237,10240,10243,10246,10249],{"title":10235,"detail":10236},"Provenance before conclusion","The interface leads with the record fields the model weighted and lets the recommendation follow from them. A conclusion presented first is something to accept or reject; evidence presented first is something to reason with.",{"title":10238,"detail":10239},"Uncertainty is content","Confidence gets the same typographic weight and the same permanence as the recommendation. It is not a tooltip, a footnote or a colour — it is part of what the model is saying.",{"title":10241,"detail":10242},"Disagreement must cost what agreement costs","Any asymmetry between the two paths is a thumb on the scale, applied thousands of times a week. Equal affordance is a safety property, not a courtesy.",{"title":10244,"detail":10245},"The model argues, it never acts","No recommendation writes to the record, schedules anything, or proceeds on a timeout. Every state change passes through a person.",{"title":10247,"detail":10248},"Do not anchor the clinician","The recommendation is withheld until the clinician records an initial impression. This costs a step and buys an independent read, which is the entire value of having a clinician in the loop.",{"title":10250,"detail":10251},"The audit record is a design deliverable","What the interface captures by default is what will exist when someone reconstructs the decision. It must be complete without anyone trying.","AI product design & interaction model",{"summary":10254,"premises":10255,"personas":10274,"journey":10301},"The premises below are the scenario we chose, drawn from the failure patterns that recur across published clinical decision-support deployments. They are assumptions we committed to and designed against, not findings from clinicians we spoke to — there were none.",[10256,10259,10262,10265,10268,10271],{"label":10257,"detail":10258},"The model is good and still ignored","We deliberately set the model's validated accuracy high. If the recommendation were often wrong, the design problem would be trivial and uninteresting: fix the model. Assuming it is right isolates the interface problem.",{"label":10260,"detail":10261},"Most rejections are not disagreements","We assume clinicians decline to engage with the recommendation far more often than they actively judge it wrong. Disengagement and disagreement look identical in the telemetry and need completely different fixes.",{"label":10263,"detail":10264},"Confidence is read categorically","We assume any percentage above roughly 80 collapses in the reader's mind to \"the computer thinks yes\", erasing the gradient the number exists to convey.",{"label":10266,"detail":10267},"The recommendation arrives before the clinician has formed a view","Current-state placement shows the model's answer at the top of the case. We assume this anchors the clinician's own assessment, which is the thing least recoverable once it happens.",{"label":10269,"detail":10270},"Disagreement is the most valuable signal in the system","A clinician who overrides the model with a reason is generating the highest-quality training and safety data available. The current design taxes exactly that behaviour.",{"label":10272,"detail":10273},"Regulatory scrutiny is a design input, not a constraint added later","The interface must produce a defensible record by default, without the clinician doing anything extra to make it defensible.",[10275,10288],{"name":10276,"role":10277,"context":10278,"goals":10279,"frustrations":10283,"quote":10287},"Dr. Yusuf Adeyemi","Emergency triage clinician","Works a twelve-hour shift with a queue that never empties, seeing each case for a few minutes. He is not hostile to the model — he simply cannot spend ninety seconds working out where a number came from.",[10280,10281,10282],"Reach a defensible decision quickly and move to the next case","Know when the model is on firm ground and when it is guessing","Leave a record that explains itself without extra typing",[10284,10285,10286],"A severity score with no indication of what drove it","Disagreeing takes four interactions; accepting takes one","The model's answer appears before he has read the case himself","I do not need it to be right. I need to be able to say why I did what I did, and right now it cannot help me with that.",{"name":10289,"role":10290,"context":10291,"goals":10292,"frustrations":10296,"quote":10300},"Marielle Koch","Clinical Safety Lead","Owns the incident review process and signs off the deployment. She is the person who will be asked to reconstruct a decision eighteen months after it was made, from whatever the interface happened to store.",[10293,10294,10295],"Reconstruct any decision from the record alone","See where the model and clinicians systematically diverge","Demonstrate that no action was taken without a person in the loop",[10297,10298,10299],"Override reasons captured as a taxonomy that fits few real cases","No record of what the model showed, only what it concluded","Disengagement and disagreement are indistinguishable in the data","If the only thing we stored is that someone clicked accept, we have not stored a decision. We have stored a click.",[10302,10305,10310,10315,10320,10325],{"stage":10303,"doing":10304,"feeling":9792},"Case enters queue","Opens the next case from the triage list.",{"stage":10306,"doing":10307,"feeling":9792,"friction":10308,"opportunity":10309},"Reads the record","Scans history, observations and presenting complaint.","The model's recommendation is already on screen, before he has formed a view.","Withhold the recommendation until the clinician has recorded an initial read.",{"stage":10311,"doing":10312,"feeling":9796,"friction":10313,"opportunity":10314},"Sees the recommendation","Reads a severity score and a suggested pathway.","No indication of what in the record produced it.","Lead with the evidence, and let the conclusion follow from it.",{"stage":10316,"doing":10317,"feeling":9796,"friction":10318,"opportunity":10319},"Looks for reasoning","Opens two other screens hunting for the model's inputs.","The reasoning is not in the product at all — it is in a model card.","Put provenance in the review surface, open by default.",{"stage":10321,"doing":10322,"feeling":9796,"friction":10323,"opportunity":10324},"Considers disagreeing","Weighs a four-interaction override against simply ignoring the panel.","The taxonomy does not describe his actual reason.","Make disagreement one interaction and let the reason be free text.",{"stage":10326,"doing":10327,"feeling":9792,"friction":10328,"opportunity":10329},"Documents own decision","Records his assessment separately, as though the model were not there.","The most valuable signal in the system — an expert override — is lost.","Capture the override as structured data without taxing the clinician.",{"title":9929,"description":10065},[10332,8182,10333],"AI Product Design","Product Strategy",[10335,10336,9825,10337],"Figma","React","FHIR","work\u002F2.meridian-clinical-ai",{"summary":10340,"radius":339,"density":10341,"palette":10342,"type":10359,"tokens":10376,"components":10400,"specimens":10419},"A restrained, high-legibility system for a screen read under time pressure at arm's length. The distinguishing decisions are a calm status ramp that never uses colour alone, generous type at bedside distance, and a confidence band component that is the only place uncertainty is expressed.","spacious",[10343,10347,10350,10353,10356],{"name":10344,"value":10345,"role":10346},"Slate ink","#0D1117","Primary surface, dark theme",{"name":10348,"value":9930,"role":10349},"Vital","Agreement and strong-signal states, dark theme",{"name":10351,"value":9931,"role":10352},"Vital (light)","The same role restepped for the light theme",{"name":10354,"value":9845,"role":10355},"Neutral","Secondary text, and the reference series in charts",{"name":10357,"value":9849,"role":10358},"Caution","Mixed signal — always paired with an icon and a word",[10360,10364,10367,10370,10373],{"name":10361,"size":10362,"weight":9855,"tracking":9861,"usage":10363},"Case title","1.75rem","Patient and case identifiers",{"name":10182,"size":10365,"weight":9855,"usage":10366},"1.25rem","The model's conclusion and the confidence band",{"name":9864,"size":10368,"weight":9866,"usage":10369},"1.0625rem","Record text, read at arm's length on a bedside tablet",{"name":10371,"size":9865,"weight":9866,"usage":10372},"Evidence","Linked record fields inside the evidence panel",{"name":10374,"size":9870,"weight":9871,"usage":10375},"Meta","Timestamps, authorship and audit references",[10377,10381,10385,10389,10393,10397],{"name":10378,"value":10379,"usage":10380},"--band-strong","signal ≥ 2 concordant","Strong signal. Definition is a token so the UI and the model share one rule.",{"name":10382,"value":10383,"usage":10384},"--band-weak","missing ≥ 1 key field","Weak signal — states what is absent rather than emitting a low number.",{"name":10386,"value":10387,"usage":10388},"--action-parity","true","Enforces identical geometry for agree and disagree. Asserted in tests.",{"name":10390,"value":10391,"usage":10392},"--evidence-default","expanded","The evidence panel's initial state. Collapse never persists across cases.",{"name":10394,"value":10395,"usage":10396},"--touch-target","44px","Minimum for gloved use on a bedside tablet.",{"name":10398,"value":1006,"usage":10399},"--auto-actions","A build-time assertion, not a preference. Nothing acts without a person.",[10401,10404,10407,10410,10413,10416],{"name":10402,"variants":47,"detail":10403},"ConfidenceBand","The single place uncertainty is expressed. Icon, word and colour, in that order.",{"name":10405,"variants":306,"detail":10406},"EvidenceItem","A weighted record field linked to its source, with the direction of its contribution.",{"name":10408,"variants":203,"detail":10409},"DecisionBar","Agree and disagree at identical weight. No primary variant exists.",{"name":10411,"variants":334,"detail":10412},"RecordSection","Collapsible record regions with a persistent summary line.",{"name":10414,"variants":290,"detail":10415},"CaseCard","Queue entry. Deliberately carries no model output.",{"name":10417,"variants":296,"detail":10418},"AuditEntry","Immutable decision record, replaying exactly what was on screen.",[9922,10420,9920,10421],"cards","chart","11-week scope","hEkl59zFryRI1zzpXKqmYdPBxOzxPSw72Kd7VzLrTOw",{"id":10425,"title":10426,"accent":10427,"accentLight":10427,"body":10428,"challenge":10503,"client":10521,"cover":10522,"coverAlt":10523,"decisions":10524,"description":10555,"excerpt":61,"extension":51,"featured":6,"features":10556,"flows":10580,"gallery":10618,"ia":10651,"industry":10704,"kind":9689,"meta":10705,"navigation":24,"order":47,"outcomes":10706,"path":10728,"platform":10729,"principles":10731,"projectType":10750,"research":10751,"seo":10825,"services":10826,"stack":10828,"stem":10830,"system":10831,"timeline":10907,"year":10908,"__hash__":10909},"work\u002Fwork\u002F3.ledgerly-onboarding.md","Rebuilding onboarding around the first useful moment","#7C3AED",{"type":36,"value":10429,"toc":10497},[10430,10434,10437,10440,10443,10447,10450,10453,10456,10459,10463,10466,10469,10472,10475,10479,10485,10491],[367,10431,10433],{"id":10432},"checking-the-brief-before-accepting-it","Checking the brief before accepting it",[39,10435,10436],{},"Ledgerly's brief is to redesign the onboarding checklist. The most useful thing\nthis project does is spend a week deciding not to.",[39,10438,10439],{},"The funnel in the scenario tells a specific story. Checklist completion is\naround two thirds, which is respectable. But users who complete the checklist\nconvert at almost the same rate as users who abandon it. That single comparison\nis the whole diagnosis: if finishing a sequence does not predict the outcome,\nthen improving how many people finish it cannot help. The checklist is doing its\njob faithfully. Its job is the problem.",[39,10441,10442],{},"This is worth dwelling on because the alternative project — better copy, a\nprogress ring, a celebratory animation on completion — is a real project that\nteams ship every quarter, and it produces a measurable improvement in a metric\nthat does not connect to anything.",[367,10444,10446],{"id":10445},"finding-the-moment","Finding the moment",[39,10448,10449],{},"The redesign needs a target, and the target is the first moment the product\nbecomes comprehensible.",[39,10451,10452],{},"For Ledgerly that is a completed reconciliation: a real match, on data the user\nrecognises, that they did not have to make by hand. Not the signup, which is\njust admission. Not the bank connection, which is cost. Not the checklist, which\nis a description of cost.",[39,10454,10455],{},"Reaching that moment takes nine steps and around 47 minutes, and eight of the\nnine steps are administration. Once the target is named, the design work is\nmostly subtraction with one question asked repeatedly: does this have to happen\nbefore the first reconciliation? Teammate invites do not. The chart of accounts\ndoes not. The accounting integration does not. Six of the nine move.",[39,10457,10458],{},"They do not disappear — that is the part worth being precise about. They are\nasked later, in the product, at the point each one is first needed. An invite\nprompt when a match needs a second opinion is a different request from the same\nprompt on screen two, even though the form is identical.",[367,10460,10462],{"id":10461},"sample-data-is-a-real-risk","Sample data is a real risk",[39,10464,10465],{},"The decision most likely to backfire is demonstrating on sample data.",[39,10467,10468],{},"The logic is sound: the highest-anxiety action in the product is handing over\nbank credentials, and the current flow asks for it before showing anything. Move\nthe demonstration first and the user decides with evidence.",[39,10470,10471],{},"But users in this scenario explicitly say they do not want a tour, and a sample\nledger is one bad execution away from being a tour. If the sample data is too\nclean, it demonstrates a product that does not exist — every transaction\nmatching neatly, no ambiguity, no partial matches. The user's real ledger will\nnot look like that, and the gap between the demo and the first real batch is\nwhere trust is lost rather than built.",[39,10473,10474],{},"So the sample is deliberately messy. It includes a partial match, one\ntransaction the product cannot classify, and a duplicate. It demonstrates the\nproduct handling difficulty, which is the only demonstration worth making.",[367,10476,10478],{"id":10477},"what-we-gave-up","What we gave up",[39,10480,10481,10484],{},[517,10482,10483],{},"A clean activation metric."," Deleting the checklist removes the number the\nteam reports weekly, and nothing replaces it as a single figure. Time to first\nreconciliation is the honest measure and it is harder to instrument, noisier,\nand much less satisfying on a slide.",[39,10486,10487,10490],{},[517,10488,10489],{},"A sense of completion."," Some users like a checklist, and progressive setup\nmeans there is no moment where a badge says setup is done. The account is\npermanently, honestly, partially configured. We think that is more truthful than\na completed list that hides the same gaps, but it does feel worse.",[39,10492,10493,10496],{},[517,10494,10495],{},"Determinism."," An inferred account map that is subtly wrong is more dangerous\nthan an empty form, because it can be accepted without being read. Marking every\ninferred row helps the careful user and the second person picking the account\nup. It does nothing for the person who clicks straight through, and that person\nexists.",{"title":46,"searchDepth":47,"depth":47,"links":10498},[10499,10500,10501,10502],{"id":10432,"depth":203,"text":10433},{"id":10445,"depth":203,"text":10446},{"id":10461,"depth":203,"text":10462},{"id":10477,"depth":203,"text":10478},{"summary":10504,"points":10505},"Ledgerly is a scenario built around the most common misdiagnosis in activation work. A team sees low completion on an onboarding checklist and concludes the checklist needs better copy and progress indicators. Looking at the funnel first tells a different story, and it is a story about sequence rather than presentation.",[10506,10509,10512,10515,10518],{"label":10507,"detail":10508},"The brief was to fix the wrong artifact","Improving the checklist would have improved a number that does not matter. In the scenario, users who complete setup convert at almost the same rate as users who abandon it — which is the signal that completion is not predictive of anything.",{"label":10510,"detail":10511},"Nine administrative steps before anything useful","Creating an organisation, inviting a teammate, connecting a bank feed, waiting for a sync, configuring a chart of accounts, connecting the accounting integration, mapping accounts, setting a rule, running a batch. Only the last one is the product.",{"label":10513,"detail":10514},"The wait is invisible and unexplained","A bank feed sync takes minutes and shows a spinner. Several users in the scenario assume the product is still installing itself and close the tab — a failure that looks like churn and is actually a missing status message.",{"label":10516,"detail":10517},"Trust has to be earned before data is handed over","Connecting a bank feed is the highest-anxiety moment in the flow, and the current design asks for it before the user has seen the product do anything at all.",{"label":10519,"detail":10520},"Setup is not one job","The person signing up and the person who will configure the chart of accounts are frequently different people. A single linear wizard assumes one person with all the answers, and stalls the moment that is false.","Ledgerly","\u002Fcase-studies\u002Fledgerly\u002Fscreens\u002Ffirst-run.webp","Ledgerly first run — a sample ledger reconciling on load",[10525,10531,10537,10543,10549],{"title":10526,"problem":10527,"decision":10528,"rationale":10529,"tradeoff":10530},"Demonstrate on labelled sample data before asking for credentials","Bank credentials are the highest-anxiety request in the product, and the current flow makes it before the user has seen anything work. That is asking for maximum trust at the moment of minimum evidence.","A sample ledger reconciles on first load, unmistakably marked as sample data, with a single action to replace it with a real connection.","It moves the demonstration before the ask. The user learns what a reconciliation is, what the product does to it, and what they would be getting — and only then decides whether to connect an account.","Sample data risks reading as a canned demo, which is exactly the \"tour\" users say they do not want. The mitigation is that it is fully interactive and one click from being replaced — but if the sample looks too tidy, it undermines the credibility it exists to build.",{"title":10532,"problem":10533,"decision":10534,"rationale":10535,"tradeoff":10536},"Delete the onboarding checklist","The checklist is well-completed and predicts nothing. Keeping it means continuing to optimise a metric that does not connect to the outcome, and it occupies the persistent panel that could show something useful.","Removed entirely. What remains to be configured surfaces in place, at the point in the product where it is relevant.","A checklist is an admission that the sequence is not self-evident. Fixing the sequence removes the need for the meta-layer describing it.","Real losses. Some users genuinely like a visible sense of progress, and the team loses a clean funnel metric that was easy to report on. Nothing replaces it as a single number, and stakeholders who track it weekly will feel that keenly.",{"title":10538,"problem":10539,"decision":10540,"rationale":10541,"tradeoff":10542},"Infer the account map, then ask for review","Configuring a chart of accounts is a decision most trial users have no basis for making, and frequently not their job at all. It is where the linear wizard stalls.","Infer a starting map from the first batch of transactions, then present it for correction after the batch runs.","Reviewing a wrong guess is a much smaller cognitive task than producing a right answer from an empty form, and the transactions themselves are the best available evidence for what the map should be.","An inferred map that is subtly wrong is more dangerous than an empty one, because it may be accepted without scrutiny. Every inferred row is marked as inferred and the decision log records it as a default rather than a choice — which helps the second person, and does nothing for the first person who clicks through.",{"title":10544,"problem":10545,"decision":10546,"rationale":10547,"tradeoff":10548},"Make the sync wait explicit","A multi-minute spinner with no status is indistinguishable from a broken installation, and in the scenario it is a meaningful share of the abandonment.","Named stages, an estimated duration, and a clear statement that the user can leave and come back. The sample ledger stays interactive throughout.","Almost all of the pain here is uncertainty rather than duration. Naming the stages converts an unexplained delay into a process, and processes are tolerable.","Stated estimates are sometimes wrong, and an estimate that overruns is worse than none. The range is deliberately wide and pessimistic, which makes the common case feel slow on paper and fast in practice.",{"title":10550,"problem":10551,"decision":10552,"rationale":10553,"tradeoff":10554},"Make setup resumable by a different person","A linear wizard assumes one person with all the answers in one sitting. In the scenario the person who signs up and the person who configures are frequently different, and often days apart.","No step blocks another. A decision log records what was chosen versus what was defaulted, and open items surface where they are relevant rather than in a queue.","It matches how the work actually gets divided, and it gives the second person the one thing they need most — knowing what the first person actually agreed to, as distinct from what the product assumed.","Without a forced sequence, an account can sit half-configured indefinitely, and there is no single moment where setup is \"done\". We judged a permanently open, honest state better than a completed checklist that hides the same gaps.","A concept study in fintech activation design — replacing a nine-step administrative setup with a path that reaches a real reconciliation in three, and deleting the checklist that was measuring the wrong thing.",[10557,10561,10565,10569,10572,10576],{"title":10558,"detail":10559,"icon":10560},"Sample-first activation","A labelled, fully interactive sample ledger that reconciles immediately, replaced by real data in one action.","lucide:play",{"title":10562,"detail":10563,"icon":10564},"Progressive configuration","Six setup tasks moved out of the wizard and surfaced at the point each one is first needed.","lucide:list",{"title":10566,"detail":10567,"icon":10568},"Explicit waits","Named sync stages with a duration estimate, replacing every indeterminate spinner in the activation path.","lucide:timer",{"title":10570,"detail":10571,"icon":10082},"Inferred account mapping","A starting map derived from the first batch, presented for review with every guessed row marked as inferred.",{"title":10573,"detail":10574,"icon":10575},"Resumable setup with a decision log","No step blocks another, and a second person can see what was chosen versus what the product assumed.","lucide:clipboard-list",{"title":10577,"detail":10578,"icon":10579},"Inline data-access scope","What each connection can read, stated beside the field requesting it rather than behind a policy link.","lucide:shield-check",[10581,10597,10607],{"title":10582,"note":10583,"before":10584,"after":10594},"Reaching the first reconciliation","The core flow. Six of the nine original steps still exist — they happen after this path completes, at the point each one is first needed.",[10585,10586,10587,10588,10589,10590,10591,10592,10593],"Sign up","Create organisation","Invite a teammate","Connect bank feed","Wait for sync","Configure chart of accounts","Connect accounting integration","Map accounts","Run first batch",[10585,10595,10596],"Run the sample batch","Connect and re-run on real data",{"title":10598,"note":10599,"before":10600,"after":10604},"Connecting a bank feed","The same underlying steps, with the wait made legible. The change is entirely in what the interface says while nothing is happening.",[10601,10602,10603],"Choose institution","Enter credentials","Spinner",[10601,10605,10602,10606],"Read the data-access scope","Watch named sync stages with an estimate",{"title":10608,"note":10609,"before":10610,"after":10615},"Setting up the chart of accounts","Changed from an authoring task to a review task. Reviewing a wrong guess is far easier than producing a right answer from nothing.",[10611,10612,10613,10614],"Open settings","Read documentation","Create each account","Map each account",[10616,10617],"Review the inferred map after the first batch","Correct what is wrong",[10619,10623,10629,10634,10640,10645],{"title":10620,"caption":10621,"src":10522,"width":9602,"alt":10622,"frame":9604,"span":9605},"First run","The sample ledger reconciles on load. It is deliberately messy — a partial match, an unclassified transfer and a duplicate — because a demo where everything matches neatly shows a product that does not exist.","Reconciliation screen showing four matched and three exception rows",{"title":10624,"caption":10625,"src":10626,"width":10627,"alt":10628,"frame":9622},"Labelled, not disguised","Sample data is marked unmistakably, with the replace action attached to the label rather than buried in settings.","\u002Fcase-studies\u002Fledgerly\u002Fcomponents\u002Ffirst-run--sample.webp",1184,"Sample data banner with a connect-your-bank action",{"title":10630,"caption":10631,"src":10632,"width":9602,"alt":10633,"frame":9604},"Sync status","Named stages and a deliberately pessimistic range. Almost all of the pain in a multi-minute wait is uncertainty rather than duration.","\u002Fcase-studies\u002Fledgerly\u002Fscreens\u002Fsync-status.webp","Bank sync with five named stages and an estimated duration",{"title":10635,"caption":10636,"src":10637,"width":10638,"alt":10639,"frame":9622},"The wait does not block","The sample ledger stays interactive throughout, and leaving is explicitly safe — the failure being designed against is the user who assumes setup has stalled and closes the tab.","\u002Fcase-studies\u002Fledgerly\u002Fcomponents\u002Fsync-status--meanwhile.webp",475,"Panel offering things to do while the sync runs",{"title":10641,"caption":10642,"src":10643,"width":9632,"alt":10644,"frame":9634},"Three fields","The entire admission process. Organisation, teammates and chart of accounts are asked later, in the product, at the point each is first needed.","\u002Fcase-studies\u002Fledgerly\u002Fmobile\u002Fsignup.webp","Mobile signup with email, password and country",{"title":10646,"caption":10647,"src":10648,"width":10649,"alt":10650,"frame":9622},"Scope, stated inline","What is accessed, beside the field that asks for it, at the moment of highest anxiety in the product.","\u002Fcase-studies\u002Fledgerly\u002Fcomponents\u002Fsignup--form.webp",346,"Signup form fields with validation and help text",[10652,10662,10672,10681,10694],{"label":10653,"detail":10654,"children":10655},"Entry","Three fields, then straight into the product. No wizard, no tour.",[10656,10659],{"label":10657,"detail":10658},"Signup","Email, password, country — nothing that can be inferred or deferred",{"label":10660,"detail":10661},"Workspace","Created automatically and renameable later",{"label":10663,"detail":10664,"children":10665},"Demonstration","A labelled sample ledger that reconciles immediately, so the product explains itself by working.",[10666,10669],{"label":10667,"detail":10668},"Sample batch","Runs on load, unmistakably marked as sample data",{"label":10670,"detail":10671},"Replace with real data","One action, available from the first screen onward",{"label":10673,"detail":10674,"children":10675},"Connection","Requested after the demonstration, with the wait made explicit.",[10676,10679],{"label":10677,"detail":10678},"Provider search","Institution picker with a stated data-access scope",{"label":10630,"detail":10680},"Named stages and an estimated duration, never a bare spinner",{"label":10682,"detail":10683,"children":10684},"Configuration","Progressive. Each item surfaces at the point the product first needs it.",[10685,10688,10691],{"label":10686,"detail":10687},"Account map","Inferred from the first batch, presented for review rather than authored",{"label":10689,"detail":10690},"Rules","Offered after a repeated manual match, not before",{"label":10692,"detail":10693},"Teammates","Offered when a match needs a second opinion",{"label":10695,"detail":10696,"children":10697},"Setup state","For the second person. What was decided, what was defaulted, what is open.",[10698,10701],{"label":10699,"detail":10700},"Decision log","Distinguishes chosen values from defaults",{"label":10702,"detail":10703},"Open items","Surfaced in place, not as a separate checklist","Fintech",{},[10707,10711,10715,10718,10721,10725],{"value":10708,"label":10709,"description":10710},"9 → 3","Steps before the first reconciliation","Six administrative steps deferred until after the user has seen the product do its job once.",{"value":10712,"label":10713,"description":10714},"6","Setup tasks moved after first value","Teammate invites, chart of accounts, reconciliation rules and three settings, each requested at the point it is first needed.",{"value":9705,"label":10716,"description":10717},"Screens in the activation path","A single progressive surface replacing a five-screen wizard and a persistent checklist panel.",{"value":1006,"label":10719,"description":10720},"Checklists","Removed entirely. It measured completion of a sequence that did not predict anything.",{"value":10722,"label":10723,"description":10724},"47 → 6 min","Modelled time to first reconciliation","A walkthrough estimate of the redesigned path against the current one, counting only unavoidable waits.",{"value":9709,"label":10726,"description":10727},"Fields required before signup completes","Email, password and country. Everything else is asked when the product needs it.","\u002Fwork\u002Fledgerly-onboarding",[9718,10730],"Responsive, mobile-first setup",[10732,10735,10738,10741,10744,10747],{"title":10733,"detail":10734},"The first useful moment is the deadline","Every step in the activation path is measured against one question — does this have to happen before the user sees a reconciliation complete? Almost nothing does.",{"title":10736,"detail":10737},"Ask for a thing at the moment it is needed","Teammate invites when there is something to collaborate on. Account mapping when the first batch needs it. A question asked in context is a different question from the same one asked in a wizard.",{"title":10739,"detail":10740},"Earn the credentials","The highest-anxiety action in the flow happens after the product has demonstrated what it does, not before.",{"title":10742,"detail":10743},"Never make waiting invisible","Every wait states what is happening, roughly how long it will take, and what the user can do meanwhile. A spinner with no duration is indistinguishable from a failure.",{"title":10745,"detail":10746},"Setup is resumable and inspectable","A second person arriving days later can see what was decided, what was defaulted, and what is still open — without resetting anything.",{"title":10748,"detail":10749},"Progress is a state of the product, not a checklist","What remains to be configured is shown where it is relevant, in the product. A separate panel tracking a sequence is an admission the sequence is not self-evident.","Activation & onboarding redesign",{"summary":10752,"premises":10753,"personas":10772,"journey":10799},"The premises below define the scenario. The central one — that there is a single identifiable moment where the product becomes comprehensible, and that everything before it is overhead — is the assumption the whole redesign rests on, so it is stated plainly rather than buried.",[10754,10757,10760,10763,10766,10769],{"label":10755,"detail":10756},"The first useful moment is the first completed reconciliation","Not the signup, not the bank connection, not the checklist. The moment a user sees a real match against their own data that they did not have to make by hand.",{"label":10758,"detail":10759},"Checklist completion does not predict conversion","We set completion deliberately high — around two thirds — so that the checklist cannot be blamed. A well-completed checklist that predicts nothing is a measurement problem, not a motivation problem.",{"label":10761,"detail":10762},"Reaching first value takes about 47 minutes","Most of it administrative, and a meaningful part of it waiting on a sync with no explanation of what is happening.",{"label":10764,"detail":10765},"Abandonment is often a misunderstanding","We assume a share of users who leave during the sync believe setup is still running. They have not rejected the product; they think it has not started.",{"label":10767,"detail":10768},"The signup and the configuration are different jobs","Often different people, sometimes days apart. Any flow that requires all answers in one sitting fails on this.",{"label":10770,"detail":10771},"Sample data is acceptable if it is labelled","We assume users will accept a demonstration on sample data before connecting a real account, provided it is unmistakably labelled and takes one action to replace.",[10773,10786],{"name":10774,"role":10775,"context":10776,"goals":10777,"frustrations":10781,"quote":10785},"Aisling Byrne","Owner, eleven-person design studio","Does the books herself on a Friday afternoon because nobody else will. She is evaluating Ledgerly in a browser tab beside four others, and will give it about ten minutes before deciding it is another thing to set up.",[10778,10779,10780],"Find out quickly whether this will actually save her Friday afternoons","Avoid connecting bank credentials to something she has not evaluated","Not have to understand accounting vocabulary to get started",[10782,10783,10784],"Asked to invite a teammate before she knows if the product works","A chart-of-accounts step she does not have an opinion about yet","A spinner with no indication of how long it will run","I do not want a tour. I want to see it do the thing once, with my numbers, and then I will decide.",{"name":10787,"role":10788,"context":10789,"goals":10790,"frustrations":10794,"quote":10798},"Tomás Ferreira","Finance operations, 60-person scale-up","Inherits the account after someone else signed up. He is the person who actually configures the chart of accounts and the reconciliation rules, and he arrives days after the trial started.",[10791,10792,10793],"Pick up a half-configured account without starting over","Map accounts properly rather than accept a guessed default","Understand what the previous person already decided",[10795,10796,10797],"A linear wizard that assumes one person answers everything at once","No record of which decisions were defaults and which were chosen","Setup state that cannot be revisited without resetting it","Someone else clicked through this. I need to know what they agreed to before I trust any of it.",[10800,10803,10808,10812,10816,10821],{"stage":10657,"doing":10801,"feeling":10802},"Enters email and password, names the organisation.","positive",{"stage":10804,"doing":10805,"feeling":9796,"friction":10806,"opportunity":10807},"Invite teammates","Asked to invite colleagues before seeing the product.","She has nothing to invite them to yet, and skipping feels like doing it wrong.","Move invites to the moment collaboration is first needed.",{"stage":10588,"doing":10809,"feeling":9796,"friction":10810,"opportunity":10811},"Asked for bank credentials on the third screen.","Highest-anxiety action in the flow, requested before any value is shown.","Demonstrate on labelled sample data first, connect afterwards.",{"stage":10589,"doing":10813,"feeling":9796,"friction":10814,"opportunity":10815},"Watches a spinner for several minutes with no status.","No indication of duration or progress. Reads as a stalled installation.","Make the wait explicit and fill it with something useful.",{"stage":10817,"doing":10818,"feeling":9796,"friction":10819,"opportunity":10820},"Configure accounts","Asked to set up a chart of accounts and map them.","A decision she has no basis for making yet, and often not her job.","Infer a starting map, defer the review until the first batch runs.",{"stage":10822,"doing":10823,"feeling":10802,"opportunity":10824},"First reconciliation","Finally sees a real match complete.","This is the moment. Everything before it is cost — move it here.",{"title":10426,"description":10555},[10827,8182,9821],"SaaS Design",[10335,10829,9825,9826],"Nuxt 3","work\u002F3.ledgerly-onboarding",{"summary":10832,"radius":1590,"density":9831,"palette":10833,"type":10847,"tokens":10865,"components":10887,"specimens":10906},"A calm, low-anxiety system for a product handling other people's money. The decisions worth pointing at are a deliberately narrow palette that reserves saturation for money movement, form fields that state their data-access scope inline, and a wait component that is a first-class part of the system rather than a spinner someone dropped in.",[10834,10836,10839,10842,10843],{"name":9834,"value":10835,"role":10346},"#111014",{"name":10837,"value":10427,"role":10838},"Ledger","Primary action. Passes contrast in both themes, so it is not restepped.",{"name":10840,"value":9931,"role":10841},"Reconciled","A completed match. The only place green appears.",{"name":10354,"value":9845,"role":10355},{"name":10844,"value":10845,"role":10846},"Unmatched","#B45309","Needs attention — never used for anything that is merely incomplete",[10848,10852,10856,10859,10862],{"name":10849,"size":10850,"weight":9855,"tracking":9861,"usage":10851},"Figure","2rem","Balances and totals, always tabular",{"name":10853,"size":10854,"weight":9855,"usage":10855},"Heading","1.375rem","Section and step headings",{"name":9864,"size":10857,"weight":9866,"usage":10858},"1rem","Running text and field labels",{"name":10860,"size":9865,"weight":9871,"usage":10861},"Amount","Transaction amounts in dense ledger rows",{"name":10863,"size":9870,"weight":9866,"usage":10864},"Caption","Scope notices, inferred-value markers and timestamps",[10866,10870,10873,10876,10880,10883],{"name":10867,"value":10868,"usage":10869},"--tabular-figures","always","Every numeral in the product. A misaligned column of money reads as an error.",{"name":10871,"value":8449,"usage":10872},"--wait-estimate","No indeterminate spinner may ship without a stated duration range.",{"name":10874,"value":6426,"usage":10875},"--scope-notice","Data-access scope sits beside the field requesting it, never in a link.",{"name":10877,"value":10878,"usage":10879},"--inferred-marker","dotted underline","Distinguishes a value the product guessed from one a person chose.",{"name":9882,"value":10881,"usage":10882},"12px","Softer than the operations tools. Deliberate — anxiety reduction.",{"name":10884,"value":10885,"usage":10886},"--destructive-confirm","typed","Anything touching a real account requires typing, not a click.",[10888,10891,10894,10897,10900,10903],{"name":10889,"variants":296,"detail":10890},"SetupCard","A progressive configuration unit. Resumable, and never blocking another.",{"name":10892,"variants":306,"detail":10893},"SyncStatus","Named stages with an estimate. Replaces every indeterminate spinner.",{"name":10895,"variants":329,"detail":10896},"LedgerRow","Matched, unmatched, partial and inferred states at three densities.",{"name":10898,"variants":47,"detail":10899},"ScopeNotice","States what data is accessed, inline with the field that requests it.",{"name":10901,"variants":203,"detail":10902},"SampleBanner","Marks sample data unmistakably, with the replace action attached.",{"name":10904,"variants":290,"detail":10905},"DecisionLog","Chosen versus defaulted, for whoever picks the account up next.",[9921,9920,10420,9923],"9-week scope","2025","7JXXMs7nVqqUvAmXmqGV0oiqO-jVJDV5jAkouWrd1-M",{"id":10911,"title":10912,"accent":9849,"accentLight":10845,"body":10913,"challenge":10986,"client":11004,"cover":11005,"coverAlt":11006,"decisions":11007,"description":11038,"excerpt":61,"extension":51,"featured":6,"features":11039,"flows":11062,"gallery":11107,"ia":11134,"industry":11183,"kind":9689,"meta":11184,"navigation":24,"order":290,"outcomes":11185,"path":11207,"platform":11208,"principles":11210,"projectType":11229,"research":11230,"seo":11307,"services":11308,"stack":11310,"stem":11312,"system":11313,"timeline":11391,"year":10908,"__hash__":11392},"work\u002Fwork\u002F4.fleetwise-dashboard.md","A fleet dashboard people stop exporting to spreadsheets",{"type":36,"value":10914,"toc":10980},[10915,10919,10922,10925,10928,10932,10935,10938,10941,10944,10947,10951,10954,10957,10964,10967,10971,10974,10977],[367,10916,10918],{"id":10917},"read-the-most-used-control","Read the most-used control",[39,10920,10921],{},"The diagnosis in this scenario takes one number: the most-used control on the\ndashboard is Export to CSV.",[39,10923,10924],{},"That single fact reframes the project. It means the product is not a dashboard\nthat needs improving — it is a query interface people cannot use, wrapped around\na data export they can. Every feature downstream of that click is unreachable\nby definition, and any redesign that makes the dashboard prettier improves the\npart nobody stays on.",[39,10926,10927],{},"It also names the competitor. Fleetwise is not losing to another vendor. It is\nlosing to Excel, and Excel is winning on three specific properties: you can\ncompose arbitrary conditions, the file persists, and it responds instantly.\nThose three are the brief. None of them is a visual design problem.",[367,10929,10931],{"id":10930},"composability-has-a-floor-and-the-floor-is-latency","Composability has a floor, and the floor is latency",[39,10933,10934],{},"The condition builder is the centrepiece, and it only works if the table is\nfast.",[39,10936,10937],{},"This is the part that makes it an engineering project as much as a design one.\nA composable query encourages exploration — add a condition, look, remove it,\ntry another. That loop is the entire value. At a hundred milliseconds it is a\nloop; at three seconds it is a series of decisions, and an operator who has to\ndecide before each query will plan carefully and try two things instead of\ntwenty.",[39,10939,10940],{},"So the interaction budget is a design requirement expressed in milliseconds, and\nit forces an architectural choice: load the working set once and filter on the\nclient. That decision buys the exploration loop and costs a heavier initial\nload, a large client-side working set, and data that can be stale between\nrefreshes.",[39,10942,10943],{},"The staleness is why freshness is displayed per view rather than as one global\ntimestamp. Having chosen an architecture that permits stale data, hiding that\nfact would be indefensible.",[39,10945,10946],{},"It is also worth saying where this stops working. At 400,000 rows the client\ncannot hold the working set and the whole model has to change — server-side\nquery with an optimistic client cache, or a fundamentally different interaction\nthat does not assume instant feedback. This design does not stretch to that; it\nwould need to be replaced.",[367,10948,10950],{"id":10949},"views-are-the-real-unit","Views are the real unit",[39,10952,10953],{},"The condition builder gets the attention, but saved views are probably the\nlarger win.",[39,10955,10956],{},"The strongest signal in the scenario is that each operator has four or five\nquestions they ask every single morning and rebuild by hand every single time.\nThat is work the product watches happen daily and refuses to remember.",[39,10958,10959,10960,10963],{},"The important decision is what a view contains. A saved ",[947,10961,10962],{},"filter"," restores the\nquery and loses the columns, the sort and the density — about a third of the\nwork, which is more irritating than restoring nothing, because the user has to\nnotice what is missing and then fix it. A view therefore carries the entire\nstate, and is addressable by URL, which as a side effect turns an escalation\nfrom a screenshot plus a paragraph of reconstruction instructions into a link.",[39,10965,10966],{},"The hidden cost is versioning. Views reference columns, and columns change. Every\nview needs to degrade gracefully and offer a repair rather than breaking, which\nis a substantial piece of engineering that shows up as no visible feature at\nall. It is the sort of thing that gets cut in week nine and regretted in month\nfour.",[367,10968,10970],{"id":10969},"leaving-the-escape-hatch-open","Leaving the escape hatch open",[39,10972,10973],{},"Export stays exactly where it is.",[39,10975,10976],{},"There is an argument for removing it — force the habit change, make the new\ntools the only path. It would improve adoption on paper. It is also punishing\nusers for a shortcoming that was the product's, and some downstream uses of an\nexport are perfectly legitimate.",[39,10978,10979],{},"The better reason to keep it is diagnostic. If export remains the first click\nafter the redesign, then the composable query did not work, and that is worth\nknowing plainly rather than obscuring by closing the alternative. A metric you\nhave removed the option to fail is not a metric.",{"title":46,"searchDepth":47,"depth":47,"links":10981},[10982,10983,10984,10985],{"id":10917,"depth":203,"text":10918},{"id":10930,"depth":203,"text":10931},{"id":10949,"depth":203,"text":10950},{"id":10969,"depth":203,"text":10970},{"summary":10987,"points":10988},"Fleetwise is a scenario about a product that works exactly as specified and is still losing to Excel. The dashboard can display 40,000 active shipments. It simply cannot do the three things an operator does with them, so the operator exports and does the real work elsewhere. The design problem is not the visual design of the dashboard; it is that the interaction model stops at \"display\".",[10989,10992,10995,10998,11001],{"label":10990,"detail":10991},"The product had become a download button","The most-used control on the page is Export to CSV. Operators load the dashboard, immediately export, and work in a spreadsheet. Every feature built downstream of that click is unused by definition.",{"label":10993,"detail":10994},"One filter at a time, against multi-condition questions","Real operational questions are compound — late shipments from one hub, excluding two carriers already known to be in a weather delay. A single-select filter bar cannot express the question, so it cannot be asked.",{"label":10996,"detail":10997},"The dashboard forgets everything on refresh","Each operator has four or five recurring questions they ask daily and rebuild by hand every morning. The product has no memory of work that repeats every single day.",{"label":10999,"detail":11000},"Comparison is impossible","Operators need two filtered sets beside each other. The product shows one, which is why the second spreadsheet window exists.",{"label":11002,"detail":11003},"Latency sets a floor under everything","Sorting takes seconds and filtering re-fetches from the server. At that cost exploration stops — nobody experiments with a query that makes them wait, so the interaction model is constrained by the data layer before any design decision is made.","Fleetwise","\u002Fcase-studies\u002Ffleetwise\u002Fscreens\u002Fcomposable-query.webp","Fleetwise composable query above a virtualised shipment table",[11008,11014,11020,11026,11032],{"title":11009,"problem":11010,"decision":11011,"rationale":11012,"tradeoff":11013},"A composable condition builder instead of curated filter presets","Presets are easier to design, easier to explain and cover the common cases. They also decide in advance which questions may be asked, and the questions that matter on an operations floor are the unusual ones.","A stackable field\u002Foperator\u002Fvalue builder with one level of and\u002For grouping, rendered along the top of the table so it reads as a sentence. Each condition is removable independently.","It makes the expressible set match the answerable set. The sentence rendering is what keeps it usable — a query builder that looks like a query builder gets used by three people, one that reads as a line of English gets used by everyone.","Higher floor than presets. A new operator faces an empty builder rather than a menu of good starting points, and one level of grouping still cannot express genuinely nested logic. Shared views are the mitigation for the first problem; the second is a deliberate cap, because two levels of nesting is where these interfaces become unreadable.",{"title":11015,"problem":11016,"decision":11017,"rationale":11018,"tradeoff":11019},"Saved views carry everything, not just the filter","A saved filter that restores the query but not the columns, sort or density restores about a third of the work, which is somehow more annoying than restoring none of it.","A view is the full state — query, column set, order, sort, density and live\u002Fpaused — addressable by URL and shareable.","It matches the unit operators actually think in. Nobody wants \"the late shipments filter\"; they want \"my morning view\", and the difference between those two is everything except the filter.","Views drift as the schema changes. A view referencing a removed column has to degrade rather than break, which means every view carries versioning and a repair path — a meaningful chunk of the build that produces no visible feature.",{"title":11021,"problem":11022,"decision":11023,"rationale":11024,"tradeoff":11025},"Client-side filtering over a virtualised 40,000-row body","Server-side filtering means a round-trip per interaction. At a hundred milliseconds or more, operators plan queries instead of exploring, and the composable builder loses most of its value.","Load the working set once, filter and sort on the client against a virtualised table body, and reconcile with the server on an explicit refresh.","It puts the interaction under the exploration threshold, which is what makes the query builder worth having. Below that threshold operators try things; above it they do not.","A real architectural cost. The client now holds a large working set, the initial load is heavier, and the data can be stale between refreshes — which is precisely why staleness is displayed per view rather than assumed. This decision would not survive a jump to 400,000 rows, and the design would need to change rather than stretch.",{"title":11027,"problem":11028,"decision":11029,"rationale":11030,"tradeoff":11031},"Live updates are opt-in, per view","A real-time dashboard sounds obviously correct and is hostile during triage. Rows moving under the pointer while an operator works a list is the fastest way to lose their place and their trust.","Views are paused by default during interaction. New data is announced as a count — \"14 new since 09:12\" — and applied when the operator asks.","It separates knowing that something changed from having the ground move. The operator keeps control of when their working set is disturbed, and still knows immediately that it is out of date.","Someone will look at a stale view and believe it is current. The age indicator on every view is the mitigation, and it is not a complete one — this is a genuine safety trade made in favour of not disrupting active work.",{"title":11033,"problem":11034,"decision":11035,"rationale":11036,"tradeoff":11037},"Keep Export to CSV, and stop measuring it as a feature","Export is the symptom, and removing it to force adoption punishes the user for the product's shortcomings. Some downstream uses are also entirely legitimate.","Export stays, unchanged and easy to find, and is treated as a diagnostic signal rather than a feature to promote.","Its position in the interaction order is the honest measure of whether the redesign worked. If it remains the first click, the composable query has failed, and that is worth knowing without ambiguity.","Leaving the escape hatch open means some operators never change their habit, and habit is strong after years. Closing it would produce better adoption numbers and worse information.","A concept study in dense operations tooling — replacing a 40,000-row logistics dashboard's single-select filter bar with a composable query, saved views and side-by-side comparison, so the spreadsheet stops winning.",[11040,11044,11048,11051,11055,11058],{"title":11041,"detail":11042,"icon":11043},"Composable condition builder","Stackable field\u002Foperator\u002Fvalue conditions with and\u002For grouping and first-class exclusions, rendered as a readable sentence.","lucide:filter",{"title":11045,"detail":11046,"icon":11047},"Saved and shareable views","Query, columns, sort, density and live state as one addressable unit, with a repair path when the schema moves under them.","lucide:bookmark",{"title":11049,"detail":11050,"icon":10082},"Side-by-side comparison","Two independent queries in one surface, each with its own column set and freshness.",{"title":11052,"detail":11053,"icon":11054},"Virtualised 40,000-row table","Client-side filtering and sorting under a 100ms interaction budget asserted in CI.","lucide:table-2",{"title":11056,"detail":11057,"icon":109},"Twelve specified table states","Including filtered-empty, partial and stale — the states that are usually left to chance and are where dense tables lose credibility.",{"title":11059,"detail":11060,"icon":11061},"Per-view freshness with opt-in live updates","Views pause during interaction and announce pending changes as a count rather than reordering rows under the pointer.","lucide:activity",[11063,11074,11085,11096],{"title":11064,"note":11065,"before":11066,"after":11071},"Asking a compound question","The before column is not a worse way of doing the same thing — the question genuinely cannot be expressed, so it gets approximated and the approximation is what gets acted on.",[11067,8708,11068,11069,11070],"Apply the one filter that fits best","Open the spreadsheet","Add an autofilter","Add the remaining conditions",[11072,11072,11073],"Add condition","Add exclusion",{"title":11075,"note":11076,"before":11077,"after":11083},"Starting a shift","Rebuilt by hand every morning in the current state, for work that is identical every day.",[11078,11079,11080,11081,11082],"Open dashboard","Re-apply four filters","Re-choose columns","Re-apply sort","Set density",[11084],"Open the pinned view",{"title":11086,"note":11087,"before":11088,"after":11093},"Escalating something to the network lead","A view being addressable turns a paragraph of reconstruction instructions into a link.",[11089,11090,11091,11092],"Screenshot the spreadsheet","Write out which filters were used","Send both","Answer follow-up questions about the filters",[11094,11095],"Copy the view URL","Send it",{"title":11097,"note":11098,"before":11099,"after":11104},"Comparing two hubs","Not an advanced feature — it is how the next hour gets allocated.",[11100,11101,11102,11103],"Export hub A","Export hub B","Open two spreadsheet windows","Arrange them side by side",[11105,11106],"Split the view","Set the second query",[11108,11113,11119,11124,11129],{"title":11109,"caption":11110,"src":11005,"width":11111,"alt":11112,"frame":9604,"span":9605},"Composable query","Conditions stacked along the top of the table, reading as a sentence, with the exclusion as a first-class condition — \"except the ones I already know about\" is most of what expertise looks like on an operations floor.",1600,"Shipment table filtered by four stacked conditions including an exclusion",{"title":11114,"caption":11115,"src":11116,"width":11117,"alt":11118,"frame":9622,"span":9605},"The query bar","Field, operator, value, each removable independently. A query builder that looks like a query builder gets used by three people; one that reads as a line of English gets used by everyone.","\u002Fcase-studies\u002Ffleetwise\u002Fcomponents\u002Fcomposable-query--query.webp",1380,"Condition builder rendered as a readable sentence",{"title":11120,"caption":11121,"src":11122,"width":11117,"alt":11123,"frame":9622},"Saved views and freshness","The unit operators actually work in. Views are paused during interaction and announce pending changes as a count, so the ground never moves under the pointer.","\u002Fcase-studies\u002Ffleetwise\u002Fcomponents\u002Fcomposable-query--views.webp","Saved view tabs with a paused freshness indicator",{"title":11125,"caption":11126,"src":11127,"width":11111,"alt":11128,"frame":9604},"Split comparison","Two independent queries in one surface, each with its own columns and its own freshness. This replaces the second spreadsheet window.","\u002Fcase-studies\u002Ffleetwise\u002Fscreens\u002Fsplit-comparison.webp","Two hub queries side by side with independent statistics",{"title":11130,"caption":11131,"src":11132,"width":11111,"alt":11133,"frame":9604,"span":9605},"Table states","Filtered-empty distinguished from empty, partial from loading, stale from current. These are the states dense tables leave to chance, and where trust is lost.","\u002Fcase-studies\u002Ffleetwise\u002Fscreens\u002Ftable-states.webp","Six specified table states shown side by side",[11135,11148,11161,11173],{"label":11136,"detail":11137,"children":11138},"Query","The composable layer that replaced the filter bar. Reads left to right as a sentence.",[11139,11142,11145],{"label":11140,"detail":11141},"Condition","Field, operator, value — each removable independently",{"label":11143,"detail":11144},"Grouping","and\u002For nesting, one level deep, shown structurally",{"label":11146,"detail":11147},"Exclusions","First-class, because \"except the ones I know about\" is most of the job",{"label":11149,"detail":11150,"children":11151},"Views","A saved query plus everything around it. The unit operators actually work in.",[11152,11155,11158],{"label":11153,"detail":11154},"Personal views","The four or five daily questions, pinned",{"label":11156,"detail":11157},"Shared views","Addressable by URL, so an escalation is a link",{"label":11159,"detail":11160},"Comparison","Two independent queries side by side in one surface",{"label":9911,"detail":11162,"children":11163},"Virtualised, dense, with the states most dense tables omit.",[11164,11167,11170],{"label":11165,"detail":11166},"Column set","Chosen per view, not globally",{"label":11168,"detail":11169},"Density","Three steps, persisted with the view",{"label":11171,"detail":11172},"States","Loading, empty, filtered-empty, error, partial, stale",{"label":11174,"detail":11175,"children":11176},"Freshness","Every view states its own age and refresh behaviour.",[11177,11180],{"label":11178,"detail":11179},"Age indicator","Per view, not a single global timestamp",{"label":11181,"detail":11182},"Live or paused","Explicit, because live updates during triage are hostile","Logistics",{},[11186,11190,11193,11196,11200,11203],{"value":11187,"label":11188,"description":11189},"1 → n","Simultaneous filter conditions","A single-select filter bar replaced by a stackable condition builder with and\u002For grouping.",{"value":10223,"label":11191,"description":11192},"Filtered sets visible at once","A split view for the comparison operators previously did by opening two spreadsheet windows.",{"value":1006,"label":11194,"description":11195},"Views lost on refresh","Saved views persist the full query, column set, sort and density, and are shareable by URL.",{"value":11197,"label":11198,"description":11199},"40,000","Rows held at interactive latency","Virtualised body with client-side filtering, so exploration does not cost a server round-trip per keystroke.",{"value":9713,"label":11201,"description":11202},"Screens to answer a recurring question","The daily questions operators asked became saved views on one surface instead of four navigations.",{"value":11204,"label":11205,"description":11206},"12","Table states specified","Loading, empty, filtered-empty, error, partial, stale and six selection states — the ones dense tables usually omit.","\u002Fwork\u002Ffleetwise-dashboard",[9718,11209],"Operations floor, large displays",[11211,11214,11217,11220,11223,11226],{"title":11212,"detail":11213},"The spreadsheet is the competitor","Every decision is measured against composition, persistence and speed — the three properties that make Excel win. Beating it on visual design is not a goal and would not help.",{"title":11215,"detail":11216},"Any question the data supports must be expressible","If an operator can describe a filter in a sentence, the interface must let them build it. Curated filter presets are a way of deciding in advance which questions are allowed.",{"title":11218,"detail":11219},"Exploration requires sub-100ms feedback","This is an interaction-design requirement expressed as a performance budget. Above the threshold, users plan queries instead of exploring, and no amount of interface quality recovers it.",{"title":11221,"detail":11222},"Everything is addressable","A view is a URL. Sharing a question should never mean describing how to reconstruct it.",{"title":11224,"detail":11225},"State is never silently lost","Query, columns, sort, density and scroll position survive a refresh. A product that forgets daily work is telling the user their work is not worth remembering.",{"title":11227,"detail":11228},"Show staleness rather than hiding it","A number with an unknown age is worse than an old number that says how old it is. Every view carries its own freshness.","Data-dense product design & front-end architecture",{"summary":11231,"premises":11232,"personas":11251,"journey":11278},"The premises are drawn from the recurring pattern in operations tooling: the spreadsheet is not winning because it is better designed, but because it is composable, persistent and fast. Those three properties are the actual competitor, and they are the brief.",[11233,11236,11239,11242,11245,11248],{"label":11234,"detail":11235},"The spreadsheet is the real competitor","Not another vendor. Any feature that does not beat Excel on composition, persistence or speed will be routed around, however well it is drawn.",{"label":11237,"detail":11238},"Operators ask compound questions","Two to four conditions, frequently including an exclusion. Exclusions matter disproportionately — \"except the ones I already know about\" is most of what expertise looks like on an operations floor.",{"label":11240,"detail":11241},"The same four or five questions recur daily","Per operator, stable over months. This is the strongest signal in the scenario: work that repeats daily and is rebuilt by hand every time.",{"label":11243,"detail":11244},"Comparison is a first-class task","Not an advanced feature. Two sets side by side is how an operator decides where to spend the next hour.",{"label":11246,"detail":11247},"Sub-100ms filtering changes behaviour, not just satisfaction","Below roughly a tenth of a second, operators explore. Above it they plan a query before running it, and exploration stops being possible.",{"label":11249,"detail":11250},"Export should remain, and stop being the first action","Some downstream uses are legitimate. Success is export moving from the first click to a deliberate late one, not disappearing.",[11252,11265],{"name":11253,"role":11254,"context":11255,"goals":11256,"frustrations":11260,"quote":11264},"Renske de Groot","Operations controller, Rotterdam hub","Runs a shift watching 40,000 active shipments on two monitors, deciding continuously where attention is worth spending. She built a personal spreadsheet system years ago and it works.",[11257,11258,11259],"Ask a compound question without planning it in advance","Return to the same five views every morning without rebuilding them","Compare two hubs side by side while a call is happening",[11261,11262,11263],"One filter at a time, against questions that need four","Everything lost on refresh","A three-second sort that makes exploration not worth attempting","I already know about the weather in Lyon. I need the list that excludes it, and there is no way to say that to this thing.",{"name":11266,"role":11267,"context":11268,"goals":11269,"frustrations":11273,"quote":11277},"Idris Mahmood","Network operations lead","Does not work the floor. Reviews patterns across hubs weekly and needs the same view the controllers are looking at, not an approximation of it rebuilt in a reporting tool.",[11270,11271,11272],"See exactly what a controller saw when they escalated","Compare this week against last on the same query","Share a view without describing how to reconstruct it",[11274,11275,11276],"Views cannot be shared, only described in a message","Reporting tool and dashboard disagree because the filters differ","No way to tell whether a number is current or stale","Someone sends me a screenshot and a paragraph explaining which filters they used. That paragraph should have been a link.",[11279,11282,11287,11292,11297,11302],{"stage":11280,"doing":11281,"feeling":9792},"Shift starts","Opens the dashboard on the left monitor.",{"stage":11283,"doing":11284,"feeling":9796,"friction":11285,"opportunity":11286},"Rebuild yesterday's view","Re-applies the same filters and column choices by hand.","Identical work, every morning, because nothing persists.","Saved views carrying the full query, columns, sort and density.",{"stage":11288,"doing":11289,"feeling":9796,"friction":11290,"opportunity":11291},"Ask a compound question","Tries to express a two-condition question with one filter.","The question cannot be stated, so it gets approximated.","A stackable condition builder that reads as a sentence.",{"stage":11293,"doing":11294,"feeling":9792,"friction":11295,"opportunity":11296},"Give up and export","Clicks Export to CSV and switches to the spreadsheet.","Everything after this point happens outside the product.","Beat the spreadsheet on composition and speed, not on features.",{"stage":11298,"doing":11299,"feeling":9796,"friction":11300,"opportunity":11301},"Compare two hubs","Opens a second spreadsheet window beside the first.","The product shows one set; comparison is not expressible in it.","A split view holding two independent queries.",{"stage":11303,"doing":11304,"feeling":9796,"friction":11305,"opportunity":11306},"Escalate","Screenshots the spreadsheet and writes a paragraph explaining the filters.","The view cannot be shared, only reconstructed from a description.","Every view addressable by URL.",{"title":10912,"description":11038},[11309,8182,9821],"Dashboard Design",[10335,9824,9825,11311,9826],"TanStack Table","work\u002F4.fleetwise-dashboard",{"summary":11314,"radius":290,"density":11315,"palette":11316,"type":11332,"tokens":11347,"components":11371,"specimens":11390},"A compact, high-density system built for a shift spent scanning. Small radii, tight control heights, hairline rules instead of zebra striping, and a status ramp deliberately held apart from the categorical chart hues so a neutral data series never reads as a warning.","compact",[11317,11321,11323,11325,11328],{"name":11318,"value":11319,"role":11320},"Console","#0A0A0C","Primary surface, dark theme — the floor runs dark all shift",{"name":9838,"value":9849,"role":11322},"Primary action and the subject series in charts, dark theme",{"name":9841,"value":10845,"role":11324},"The same role restepped — bright amber is unreadable on white",{"name":11326,"value":9845,"role":11327},"Reference","Secondary text, and the comparison series in every chart",{"name":11329,"value":11330,"role":11331},"Exception","#DC2626","Reserved for exceptions. Never used as a categorical series colour.",[11333,11335,11338,11341,11344],{"name":9309,"size":10362,"weight":9855,"tracking":9861,"usage":11334},"Headline counts, always tabular",{"name":10853,"size":11336,"weight":9855,"usage":11337},"1.125rem","Panel and view titles",{"name":9864,"size":11339,"weight":9866,"usage":11340},"0.875rem","Table cells at default density",{"name":11342,"size":9870,"weight":9866,"usage":11343},"Dense","Table cells at compact density, where most of a shift is spent",{"name":11345,"size":9870,"weight":9866,"usage":11346},"Identifier","Shipment references and codes, monospaced for scanning",[11348,11352,11356,11360,11363,11367],{"name":11349,"value":11350,"usage":11351},"--row-height-compact","28px","Compact density. Fits roughly 40 rows on a floor monitor.",{"name":11353,"value":11354,"usage":11355},"--interaction-budget","100ms","A budget, asserted in CI. Above it, exploration stops.",{"name":11357,"value":11358,"usage":11359},"--grid-line","hairline","Row separation. Zebra striping doubles the ink for no added legibility.",{"name":11361,"value":8808,"usage":11362},"--numeric-align","Every numeric column, with tabular figures. Non-negotiable.",{"name":11364,"value":11365,"usage":11366},"--status-ramp","reserved","Held apart from categorical hues so a series never reads as a warning.",{"name":11368,"value":11369,"usage":11370},"--live-default","paused","Views do not move under the pointer unless asked.",[11372,11375,11378,11381,11384,11387],{"name":11373,"variants":1590,"detail":11374},"DataTable","Three densities, virtualised body, and the loading, empty, filtered-empty, error, partial and stale states.",{"name":11376,"variants":334,"detail":11377},"ConditionRow","Field, operator and value across six field types, including exclusions.",{"name":11379,"variants":296,"detail":11380},"ViewTab","Saved view with its own freshness indicator and live\u002Fpaused state.",{"name":11382,"variants":47,"detail":11383},"SplitPane","Two independent queries side by side, each with its own column set.",{"name":11385,"variants":290,"detail":11386},"FreshnessBadge","Age, pending-change count and refresh affordance, per view.",{"name":11388,"variants":306,"detail":11389},"SeriesChart","Subject and reference series only. One axis, always.",[1278,10421,9922,9923],"12-week scope","qkMTo1ITSsSKzCuHzK54bP3IRQGZp5PSh0AyipmTvIA",{"id":11394,"title":11395,"accent":11396,"accentLight":11397,"body":11398,"challenge":11474,"client":11495,"cover":11496,"coverAlt":11497,"decisions":11498,"description":11529,"excerpt":61,"extension":51,"featured":6,"features":11530,"flows":11553,"gallery":11591,"ia":11623,"industry":11673,"kind":9689,"meta":11674,"navigation":24,"order":296,"outcomes":11675,"path":11694,"platform":11695,"principles":11698,"projectType":11717,"research":11718,"seo":11795,"services":11796,"stack":11797,"stem":11799,"system":11800,"timeline":11877,"year":9925,"__hash__":11878},"work\u002Fwork\u002F5.verdigris-marketplace.md","An e-commerce system where every product is one of one","#E07A5F","#C2532F",{"type":36,"value":11399,"toc":11468},[11400,11404,11407,11410,11413,11416,11420,11423,11426,11429,11432,11436,11439,11442,11449,11452,11456,11459,11462,11465],[367,11401,11403],{"id":11402},"when-the-standard-patterns-are-the-problem","When the standard patterns are the problem",[39,11405,11406],{},"Most commerce design is a matter of applying well-understood patterns\ncompetently. Verdigris was chosen as a scenario because it breaks them at the\nfoundation.",[39,11408,11409],{},"A variant selector has nothing to express. A restock alert is a promise that\ncannot be kept. Review aggregation has no subject — nobody else has bought this\nsideboard and nobody else ever will. \"Customers also bought\" implies\nsubstitutability that does not exist. Comparison tables assume shared\nattributes across fungible units.",[39,11411,11412],{},"Removing all of that leaves a large hole in the middle of the listing page, and\nthe interesting question is what belongs there. The answer is the question the\nbuyer is actually asking, which is not \"which configuration do I want\" but \"is\nthis specific object, in this specific condition, acceptable to me at this\nprice\".",[39,11414,11415],{},"That reframing is the entire project. Everything else follows.",[367,11417,11419],{"id":11418},"disclosure-as-a-costly-signal","Disclosure as a costly signal",[39,11421,11422],{},"The central bet is that photographing flaws builds more trust than hiding them,\nand it is worth being explicit that this is a bet with a real cost.",[39,11424,11425],{},"The reasoning runs through signalling. A defensive buyer — and the scenario\nassumes buyers are defensive, because they have been burned — reads every\nlisting as advocacy. Beautiful photographs are exactly what a careless seller\nand a meticulous seller both produce, so they carry almost no information. The\nbuyer cannot distinguish the two, so they discount both.",[39,11427,11428],{},"A close photograph of a veneer chip is different. It costs the seller something\nto publish, and only a seller who is confident in the rest of the piece will\npay that cost. It is therefore credible in a way that no amount of reassuring\ncopy can be.",[39,11430,11431],{},"The cost is that listings look worse. In a grid, beside a competitor that\npermits flattering photography only, a Verdigris listing is at a disadvantage.\nSome sellers will leave. The ones most likely to leave are the ones with the\nmost to conceal, which is the intended selection effect — but a marketplace with\nfewer listings is a worse marketplace, and this decision would be under\ncommercial pressure from launch.",[367,11433,11435],{"id":11434},"making-the-honest-path-the-fast-path","Making the honest path the fast path",[39,11437,11438],{},"The disclosure system fails entirely if it depends on sellers being virtuous.",[39,11440,11441],{},"Dermot lists thirty pieces a month from a warehouse on his phone. He is not\ndishonest; he is busy. Any requirement that makes listing slower will be met by\nlistings sitting in draft, and any policy that is enforced by moderation rather\nthan structure will be gamed within a month.",[39,11443,11444,11445,11448],{},"So the intake is guided capture: it prompts for the specific shots that answer\nthe questions buyers ask, and flaw declaration is tapping a location on a\ndiagram and taking a photograph. This is ",[947,11446,11447],{},"one step longer"," than the current\nfree-text flow and materially faster in practice, because the slow part of the\nold flow was never the typing — it was inventing a vocabulary from nothing and\ndeciding how to describe a chip.",[39,11450,11451],{},"The argument that persuades a dealer is not that disclosure builds trust. It is\nthat photographing the scratch stops him answering the same three questions\ntwenty times a week.",[367,11453,11455],{"id":11454},"the-decision-most-likely-to-be-reversed","The decision most likely to be reversed",[39,11457,11458],{},"No countdowns, no view counters, no \"two other people are looking at this\".",[39,11460,11461],{},"One-of-one inventory is the most legitimate scarcity in retail, and the standard\nurgency toolkit is right there and would work. Adding it would raise near-term\nconversion, and someone will propose it in the first quarter after launch.",[39,11463,11464],{},"The argument against it has to be made in the right currency. It is not that\nurgency tactics are distasteful. It is that the platform's entire model rests on\nits condition grades being believed, and manufactured urgency is detectable. A\nbuyer who catches one false signal re-reads every other claim as a tactic —\nincluding the grade, including the flaw photographs, including the measurements.\nThe thing that gets spent is the credibility of the disclosure system, and that\nis the asset.",[39,11466,11467],{},"That is a hard argument to win against a conversion number, and it is the one\nthis design is least confident of surviving.",{"title":46,"searchDepth":47,"depth":47,"links":11469},[11470,11471,11472,11473],{"id":11402,"depth":203,"text":11403},{"id":11418,"depth":203,"text":11419},{"id":11434,"depth":203,"text":11435},{"id":11454,"depth":203,"text":11455},{"summary":11475,"points":11476},"Verdigris is a scenario chosen because it breaks e-commerce conventions at the foundations rather than at the edges. Nearly every established pattern — size and colour variants, restock alerts, \"customers also bought\", review aggregation, comparison tables — assumes the next unit is identical to this one. When each item is the only one that will ever exist, those patterns are not merely unhelpful; several of them are actively misleading.",[11477,11480,11483,11486,11489,11492],{"label":11478,"detail":11479},"There are no variants, so there is no variant selector","The single most established component in commerce has nothing to express. What occupies that slot instead is the question the buyer actually has, which is whether this specific object is acceptable.",{"label":11481,"detail":11482},"Condition is the product","For a mass-produced item, condition is a footnote — it is new. For a 1960s teak sideboard, the pattern of wear is most of what distinguishes this one from another, and it is what the price rests on.",{"label":11484,"detail":11485},"Photographs cannot convey scale","The most common failure in this category is a piece that does not fit, or that dominates a room in a way the images never suggested. Dimensions in millimetres are accurate and almost useless to a person imagining their hallway.",{"label":11487,"detail":11488},"Reviews cannot work","Nobody else has bought this item, so there is no social proof for the thing being sold. Trust has to be constructed from the seller's history and the quality of disclosure instead.",{"label":11490,"detail":11491},"Scarcity is real, which makes manufacturing it worse","One of one is genuinely one of one. Layering a countdown on top of a true constraint corrodes the credibility of the true constraint, which is the platform's main asset.",{"label":11493,"detail":11494},"Disclosure is against the seller's short-term interest","Every flaw photographed is a reason not to buy today. The system has to make thorough disclosure the easier path, because arguing that it pays off later will not survive contact with a dealer listing thirty pieces a month.","Verdigris","\u002Fcase-studies\u002Fverdigris\u002Fscreens\u002Flisting.webp","Verdigris listing with condition and declared flaws above the description",[11499,11505,11511,11517,11523],{"title":11500,"problem":11501,"decision":11502,"rationale":11503,"tradeoff":11504},"A fixed condition vocabulary instead of free-text description","\"Good vintage condition\" means whatever the seller wants and whatever the buyer fears. Free text makes two listings incomparable, which is the single largest source of uncertainty in the category.","Six grades with written definitions and worked photographic examples, selected rather than typed, applied identically across every listing.","A shared vocabulary makes listings comparable and makes the grade falsifiable — a buyer can check the piece against the definition, which is impossible with prose. It also removes the burden of invention from the seller.","Six grades cannot describe everything, and genuinely unusual pieces get flattened into a category that fits imperfectly. Free-text notes remain, but they sit below the grade rather than replacing it — some nuance is lost, and the comparability is worth it.",{"title":11506,"problem":11507,"decision":11508,"rationale":11509,"tradeoff":11510},"Flaws photographed and shown first","Every incentive points at photographing a piece at its best. That produces listings a defensive buyer discounts entirely, which hurts the honest seller most.","Each declared flaw must carry a close photograph at a stated distance, and the flaw gallery appears near the top of the listing rather than at the end.","Showing the worst first is a costly signal, and costly signals are the only ones a defensive buyer believes. It also answers the questions that otherwise arrive as messages, which is the argument that persuades the dealer.","Listings look worse at a glance, and in a grid of competing platforms that is a real cost. There will be sellers who leave for a platform that lets them show only the good side, and the ones most likely to leave are the ones with the most to hide — which is the intended selection effect, but it does mean fewer listings.",{"title":11512,"problem":11513,"decision":11514,"rationale":11515,"tradeoff":11516},"Generated scale references rather than dimension text","Dimensions in millimetres are accurate and do not produce a mental image. Misjudged size is the most expensive failure in the category, because the return is a freight movement.","Two references generated from the measured dimensions — a human figure at average height, and a standard doorway — plus optional comparison against room constraints the buyer has saved.","It converts a number into something a person can judge instantly, using the measurements the seller already has to supply. The doorway comparison also silently answers the question that causes the worst outcome: whether it will physically get into the house.","Entirely dependent on measurement accuracy, and it makes a bad measurement look authoritative. Dimensions are therefore required at intake with a guided capture step, and a listing with disputed measurements loses the references rather than showing approximate ones.",{"title":11518,"problem":11519,"decision":11520,"rationale":11521,"tradeoff":11522},"Alternatives that state their difference, not \"similar items\"","A recommendation rail on a unique-goods marketplace implies substitutes that do not exist. Presenting a different sideboard as \"similar\" is a claim the platform cannot support, and it undermines the one-of-one premise the whole business rests on.","A rail of the closest available pieces, each labelled with how it differs — shorter by 200mm, one grade below, different period.","It is honest about substitution being imperfect while still being useful. The buyer is comparing anyway; the platform may as well do the comparison accurately rather than implying equivalence.","Stating the difference makes the alternative less appealing, which will lower engagement with the rail. That is the correct trade — a rail that works by obscuring difference is selling the wrong thing.",{"title":11524,"problem":11525,"decision":11526,"rationale":11527,"tradeoff":11528},"No manufactured urgency","One-of-one inventory is the most legitimate scarcity in retail, and the standard toolkit — countdowns, view counters, \"two others are looking\" — is available and would work in the short term.","One plain line stating the piece is unique and unrepeatable. No timers, no counters, no artificial deadlines anywhere in the funnel.","The platform's asset is that its claims can be believed. Manufactured urgency is detectable, and once a buyer catches one false signal they re-read every true one as a tactic — including the condition grades the entire model depends on.","A measurable amount of near-term conversion left on the table, and this is the decision most likely to be reversed under commercial pressure. The defence has to be made in terms of the credibility of the condition system, because that is the thing that would actually be spent.","A concept study in marketplace design for non-fungible inventory — a vintage furniture platform where condition, scale and provenance are the purchase decision, and every conventional commerce pattern breaks.",[11531,11535,11538,11542,11546,11549],{"title":11532,"detail":11533,"icon":11534},"Six-grade condition system","A fixed vocabulary with written definitions and worked photographic examples, making two listings comparable for the first time.","lucide:badge-check",{"title":11536,"detail":11537,"icon":10074},"Located, photographed flaws","Declared damage mapped onto a diagram of the piece and photographed close at a stated distance, shown before the beauty shots.",{"title":11539,"detail":11540,"icon":11541},"Generated scale references","A human figure and a standard doorway derived from measured dimensions, plus comparison against saved room constraints.","lucide:ruler",{"title":11543,"detail":11544,"icon":11545},"Structured provenance","Nine fields captured at intake, filterable across the catalogue because they are data rather than description.","lucide:milestone",{"title":11547,"detail":11548,"icon":10082},"Honest alternatives rail","The closest available pieces, each labelled with exactly how it differs rather than presented as similar.",{"title":11550,"detail":11551,"icon":11552},"Guided mobile intake","Prompted capture that makes thorough disclosure the fastest way to list, because policy alone will not survive thirty pieces a month.","lucide:smartphone",[11554,11566,11577],{"title":11555,"note":11556,"before":11557,"after":11563},"Judging whether a piece is acceptable","The before column is forensic work the buyer performs on the seller's behalf. Every step of it is a chance to give up.",[11558,11559,11560,11561,11562],"Read a vague description","Zoom into each photograph hunting for wear","Message the seller","Wait for a reply","Re-read the answer against the images",[11564,11565],"Read the grade and its definition","Look at the photographed flaws",{"title":11567,"note":11568,"before":11569,"after":11574},"Working out whether it fits","This is where pieces get abandoned, and it is entirely a translation problem — the data was always there.",[11570,11571,11572,11573],"Read dimensions in millimetres","Find a tape measure","Measure the space","Try to picture the piece",[11575,11576],"Read the scale comparison","Check it against saved room constraints",{"title":11578,"note":11579,"before":11580,"after":11586},"Listing a piece as a dealer","One step longer than before, and faster in practice — guided capture replaces the part where he invents his own description from nothing.",[11581,11582,11583,11584,11585],"Photograph the piece","Write a free-text description","Guess at condition wording","Type dimensions","Publish",[11587,11588,11589,11590,11585],"Guided capture prompts each required shot","Tap each flaw location and photograph it","Select a condition grade","Enter structured measurements",[11592,11596,11602,11607,11612,11617],{"title":11593,"caption":11594,"src":11496,"width":9602,"alt":11595,"frame":9604,"span":9605},"Listing","Condition above description, flaw thumbnails before the beauty shots. The slot a variant selector would occupy is filled by the question a buyer of a one-of-one object actually has.","Vintage sideboard listing with condition grade and declared flaws",{"title":11597,"caption":11598,"src":11599,"width":11600,"alt":11601,"frame":9622},"Condition, graded and located","Six grades with a written definition, and every declared flaw numbered, measured and photographed at a stated distance.","\u002Fcase-studies\u002Fverdigris\u002Fcomponents\u002Flisting--condition.webp",658,"Condition grade three of six with three located flaws",{"title":11603,"caption":11604,"src":11605,"width":11600,"alt":11606,"frame":9622},"Scale you can feel","Generated from the measured dimensions. The doorway comparison quietly answers the question that causes the most expensive returns — whether it will physically get into the house.","\u002Fcase-studies\u002Fverdigris\u002Fcomponents\u002Flisting--scale.webp","Scale reference comparing the piece against a doorway and a figure",{"title":11608,"caption":11609,"src":11610,"width":11600,"alt":11611,"frame":9622,"span":9605},"Scarcity, stated once","One plain line. No countdown, no view counter — manufactured urgency is detectable, and what it spends is the credibility of the condition grades.","\u002Fcase-studies\u002Fverdigris\u002Fcomponents\u002Flisting--decision.webp","Listing decision column with a single one-of-one line",{"title":11613,"caption":11614,"src":11615,"width":9632,"alt":11616,"frame":9634},"Seller intake","Tap the flaw's location, then photograph it. One step longer than the free-text flow it replaces and faster in practice, because the slow part was never the typing.","\u002Fcase-studies\u002Fverdigris\u002Fmobile\u002Fintake.webp","Mobile seller intake with a tap-to-locate flaw diagram",{"title":11618,"caption":11619,"src":11620,"width":11621,"alt":11622,"frame":9622},"Publication is blocked","A declared flaw without a photograph cannot ship. Policy alone will not survive thirty pieces a month, so the structure enforces it.","\u002Fcase-studies\u002Fverdigris\u002Fcomponents\u002Fintake--diagram.webp",354,"Flaw location diagram with three numbered pins",[11624,11636,11652,11661],{"label":11625,"detail":11626,"children":11627},"Discovery","Filtering on the attributes that actually decide a purchase, rather than on price and category alone.",[11628,11631,11634],{"label":11629,"detail":11630},"Dimensional filters","Will-it-fit constraints entered once and applied across browsing",{"label":11632,"detail":11633},"Condition grade","Filter on the fixed vocabulary, not on free text",{"label":10145,"detail":11635},"Period, maker, material — filterable because they are fields",{"label":11637,"detail":11638,"children":11639},"The listing","One surface holding everything needed to decide, with disclosure above persuasion.",[11640,11643,11646,11649],{"label":11641,"detail":11642},"Condition summary","Grade, definition, and a map of declared flaws on the piece",{"label":11644,"detail":11645},"Flaw gallery","Each declared flaw photographed close, at a stated distance",{"label":11647,"detail":11648},"Scale","Generated comparisons against a figure and a standard doorway",{"label":11650,"detail":11651},"Provenance timeline","Period, maker, prior restoration, ownership where known",{"label":8183,"detail":11653,"children":11654},"Rebuilt from seller behaviour, because item-level reviews cannot exist.",[11655,11658],{"label":11656,"detail":11657},"Seller record","History, dispute rate and disclosure completeness",{"label":11659,"detail":11660},"Disclosure score","How thoroughly this listing documents its own faults",{"label":11613,"detail":11662,"children":11663},"Mobile-first, structured, and designed so the thorough path is the quick one.",[11664,11667,11670],{"label":11665,"detail":11666},"Guided capture","Prompts for the specific shots that answer buyer questions",{"label":11668,"detail":11669},"Flaw declaration","Tap the location on the piece, then photograph it",{"label":11671,"detail":11672},"Measurement","Structured dimensions that generate the scale references","E-commerce",{},[11676,11679,11682,11685,11688,11691],{"value":10712,"label":11677,"description":11678},"Standardised condition grades","A fixed vocabulary with written definitions and worked examples, replacing free-text seller descriptions.",{"value":10211,"label":11680,"description":11681},"Listings requiring photographed flaws","Publication is blocked until every declared flaw carries a close photograph at a stated distance.",{"value":9713,"label":11683,"description":11684},"Screens to judge a piece","Condition, dimensions, provenance and flaw detail consolidated onto the listing itself.",{"value":1006,"label":11686,"description":11687},"Countdown timers or scarcity pressure","One-of-one inventory is stated as a fact. Nothing in the interface manufactures urgency on top of it.",{"value":10223,"label":11689,"description":11690},"Scale references per listing","A human figure and a standard doorway, generated from the measured dimensions rather than illustrated.",{"value":6916,"label":11692,"description":11693},"Provenance fields captured at intake","Period, maker, materials, origin, prior restoration and four more — structured, so they are filterable rather than prose.","\u002Fwork\u002Fverdigris-marketplace",[11696,11697],"Responsive web","Seller intake, mobile",[11699,11702,11705,11708,11711,11714],{"title":11700,"detail":11701},"Lead with the flaw","Declared damage appears near the top of the listing and early in the gallery, not at the end of a description. A platform that shows the worst of a piece first is one whose photographs can be believed.",{"title":11703,"detail":11704},"Every listing is the only one","No restock, no variants, no substitutes presented as equivalents. The interface never implies that another identical unit exists, because none does.",{"title":11706,"detail":11707},"Dimensions must be felt","Every measurement is rendered against something known. A number alone is accurate and useless, and this is the failure that generates returns.",{"title":11709,"detail":11710},"Scarcity is stated, never manufactured","One of one is a fact and gets one plain line. No countdowns, no view-counters, no \"three people are looking at this\".",{"title":11712,"detail":11713},"Disclosure must be the fast path","If thorough listing is slower than careless listing, the platform will fill with careless listings regardless of policy. Structure and defaults do the work that exhortation cannot.",{"title":11715,"detail":11716},"Provenance is structured, not prose","Period, maker, materials and restoration history are fields. As prose they are unfilterable, uncomparable, and quietly omitted.","Marketplace product design & commerce system",{"summary":11719,"premises":11720,"personas":11739,"journey":11766},"The premises describe a market where the decisive information is exactly the information a seller is least motivated to supply. Most of the design work follows from taking that conflict seriously rather than assuming good behaviour.",[11721,11724,11727,11730,11733,11736],{"label":11722,"detail":11723},"The purchase decision is condition, scale and provenance","In that order. Price is a filter applied before the decision, not the decision itself.",{"label":11725,"detail":11726},"Buyers have been burned and shop defensively","We assume a buyer arrives assuming the photographs flatter the piece. The interface is read as advocacy unless it visibly works against its own interest.",{"label":11728,"detail":11729},"A photographed flaw builds more trust than a flawless listing","The central bet of the project. Visible, specific disclosure reads as competence; an unblemished listing reads as either a very good piece or a careless one, and the buyer cannot tell which.",{"label":11731,"detail":11732},"Millimetres do not communicate size","We assume almost nobody converts a dimension into a mental image reliably. A comparison against something known does the work a number cannot.",{"label":11734,"detail":11735},"Sellers optimise for listing speed","A dealer listing thirty pieces a month will take whatever path is fastest. Disclosure has to be on that path, not beside it.",{"label":11737,"detail":11738},"Returns are catastrophic in this category","A returned sideboard is a logistics event, not a postage label. Every pre-purchase question answered is disproportionately valuable.",[11740,11753],{"name":11741,"role":11742,"context":11743,"goals":11744,"frustrations":11748,"quote":11752},"Neve Ashworth","Buyer, furnishing a first house","Browsing in the evenings over several weeks, saving pieces she cannot commit to. She was burned once by a listing that described a veneer chip as \"light wear\", and now reads every listing looking for what is not being said.",[11745,11746,11747],"Know exactly what she is getting before committing","Understand whether it physically fits, and how it will feel in the room","Judge whether the price is fair for this specific condition",[11749,11750,11751],"Vague condition language that means nothing consistent","Flattering photographs at angles chosen to hide wear","Dimensions she cannot translate into her hallway","Show me the worst bit. If you will not show me the worst bit, I assume it is bad.",{"name":11754,"role":11755,"context":11756,"goals":11757,"frustrations":11761,"quote":11765},"Dermot Kavanagh","Dealer, lists around thirty pieces a month","Runs a small restoration business and lists from a warehouse on his phone. He is not trying to mislead anyone — he is trying to get thirty pieces online before Thursday, and every extra field is a reason to leave a listing in draft.",[11758,11759,11760],"List a piece in a few minutes without a desktop","Avoid questions in messages that the listing should have answered","Have his thoroughness visible to buyers who are comparing sellers",[11762,11763,11764],"Long forms that assume he is at a computer","Free-text condition fields where he has to invent his own vocabulary","No credit for disclosing more than the seller next to him","I will photograph every scratch if it stops me answering the same three questions twenty times a week.",[11767,11770,11775,11780,11785,11790],{"stage":11768,"doing":11769,"feeling":10802},"Browse","Scrolls a category, saving pieces that look promising.",{"stage":11771,"doing":11772,"feeling":9792,"friction":11773,"opportunity":11774},"Open a listing","Reads the description and works through the photographs.","Photographs are chosen to flatter; the description says \"good vintage condition\".","A fixed condition vocabulary with definitions, and flaws photographed.",{"stage":11776,"doing":11777,"feeling":9796,"friction":11778,"opportunity":11779},"Judge condition","Zooms into images hunting for wear the seller has not mentioned.","She is doing forensic work the listing should have done for her.","Lead with the flaws, mapped to their location on the piece.",{"stage":11781,"doing":11782,"feeling":9796,"friction":11783,"opportunity":11784},"Work out the size","Reads dimensions in millimetres, tries to picture the hallway.","Accurate numbers, no intuition. This is where the piece gets abandoned.","Generate scale references from the measured dimensions.",{"stage":11786,"doing":11787,"feeling":9796,"friction":11788,"opportunity":11789},"Look for reassurance","Looks for reviews, finds none, because nobody has bought this item.","The standard trust mechanism does not apply to unique goods.","Move trust to seller history and measurable disclosure quality.",{"stage":11791,"doing":11792,"feeling":9796,"friction":11793,"opportunity":11794},"Hesitate","Saves the piece and closes the tab, intending to come back.","Unresolved uncertainty, not price. The questions were never answered.","Answer them in the listing so the decision can be made once.",{"title":11395,"description":11529},[8182,9820,9821],[10335,11798,9825,9826],"Nuxt 4","work\u002F5.verdigris-marketplace",{"summary":11801,"radius":2893,"density":10341,"palette":11802,"type":11817,"tokens":11834,"components":11857,"specimens":11876},"An editorial, generous system built to let photography carry the page. Larger radii and spacious density, a warm neutral ground rather than the clinical white most commerce defaults to, and a condition component that is the only place grades are expressed anywhere in the product.",[11803,11807,11810,11812,11814],{"name":11804,"value":11805,"role":11806},"Umber","#17120F","Primary surface, dark theme — warm rather than neutral black",{"name":11808,"value":11396,"role":11809},"Patina","Primary action, dark theme",{"name":11811,"value":11397,"role":10352},"Patina (light)",{"name":11813,"value":9845,"role":10355},"Stone",{"name":11815,"value":10845,"role":11816},"Declared","Flaw markers. Informational, never a warning — a flaw is not an error.",[11818,11822,11825,11827,11830],{"name":11819,"size":11820,"weight":9871,"tracking":9861,"usage":11821},"Piece title","2.25rem","The name of the object, set lighter than a heading would normally be",{"name":11823,"size":10365,"weight":9855,"usage":11824},"Section","Condition, provenance and dimensions headings",{"name":9864,"size":10368,"weight":9866,"usage":11826},"Descriptions and provenance notes, at reading measure",{"name":11828,"size":9865,"weight":9866,"usage":11829},"Specification","Dimensions, materials and structured provenance fields",{"name":11831,"size":9870,"weight":9855,"tracking":11832,"usage":11833},"Grade","0.06em","CONDITION GRADE — set in caps, one place only",[11835,11839,11842,11846,11849,11853],{"name":11836,"value":11837,"usage":11838},"--grade-vocabulary","6 fixed","The only condition vocabulary in the product. Free text may not replace it.",{"name":11840,"value":8449,"usage":11841},"--flaw-photo","Publication is blocked until every declared flaw carries a photograph.",{"name":11843,"value":11844,"usage":11845},"--scale-reference","figure + doorway","Generated from measured dimensions. Absent if measurements are disputed.",{"name":11847,"value":1006,"usage":11848},"--urgency-signals","A build-time assertion. No countdown or view-counter component exists.",{"name":11850,"value":11851,"usage":11852},"--image-ratio","4:5","Portrait by default. Furniture is taller than a landscape crop admits.",{"name":11854,"value":11855,"usage":11856},"--radius-surface","16px","Softer than the operations tools — this is a browsing surface, not a console.",[11858,11861,11864,11867,11870,11873],{"name":11859,"variants":306,"detail":11860},"ConditionGrade","The six grades with definitions. The only place a grade is expressed.",{"name":11862,"variants":290,"detail":11863},"FlawMarker","A located flaw on the piece diagram, linked to its close photograph.",{"name":11865,"variants":47,"detail":11866},"ScaleReference","Figure, doorway and saved room constraint, generated from measurements.",{"name":11868,"variants":296,"detail":11869},"ProvenanceTimeline","Period, maker, restoration and ownership as structured, filterable events.",{"name":11871,"variants":315,"detail":11872},"PieceCard","Grid, saved, sold and reserved states. No restock or variant states exist.",{"name":11874,"variants":47,"detail":11875},"AlternativeRail","Closest available pieces, each labelled with how it differs.",[10420,9920,9921,9923],"10-week scope","dB2mYxTu6kiWPNXnPgU6HZvf3kCQwDgPCbKZty7tWj8",{"id":11880,"title":11881,"accent":11882,"accentLight":11883,"body":11884,"challenge":11963,"client":11984,"cover":11985,"coverAlt":11986,"decisions":11987,"description":12018,"excerpt":61,"extension":51,"featured":6,"features":12019,"flows":12040,"gallery":12085,"ia":12117,"industry":12166,"kind":9689,"meta":12167,"navigation":24,"order":306,"outcomes":12168,"path":12188,"platform":12189,"principles":12193,"projectType":12212,"research":12213,"seo":12287,"services":12288,"stack":12289,"stem":12292,"system":12293,"timeline":11391,"year":9925,"__hash__":12364},"work\u002Fwork\u002F6.halyard-progressive-delivery.md","A progressive delivery console that can answer \"who is seeing this?\"","#818CF8","#4F46E5",{"type":36,"value":11885,"toc":11957},[11886,11890,11893,11896,11899,11902,11906,11909,11912,11915,11918,11922,11925,11928,11935,11938,11941,11945,11948,11951,11954],[367,11887,11889],{"id":11888},"designing-for-the-reader-not-the-author","Designing for the reader, not the author",[39,11891,11892],{},"Feature-flag tools are built by people configuring rollouts and used, at the\nmoments that matter, by people diagnosing them. Those are different jobs with\nopposite requirements, and the category consistently optimises for the first.",[39,11894,11895],{},"Configuration is calm. The person doing it understands the cohorts, has time to\nread, and benefits from a faithful, complete representation of every rule. That\nis what these consoles provide, and they provide it well.",[39,11897,11898],{},"Diagnosis is not calm. It happens at 3am, to someone who did not configure the\nrollout and has no idea why the cohorts are shaped as they are, while a graph\ngoes the wrong way. That person does not want a faithful representation of the\nrules. They want one sentence: here is who is affected.",[39,11900,11901],{},"Halyard is designed around the second reader, and the first reader is treated as\nthe easier case — they get the rules, one level down, where someone with time\nwill happily click.",[367,11903,11905],{"id":11904},"computing-the-answer-changes-what-the-tool-is","Computing the answer changes what the tool is",[39,11907,11908],{},"Leading with a computed audience sounds like a presentation choice. It is\nactually the architectural decision the whole project turns on.",[39,11910,11911],{},"A console that displays rules is unfalsifiable. It shows you what is stored, and\nwhat is stored is by definition correct. There is nothing to check.",[39,11913,11914],{},"A console that computes an audience is making a claim, and a claim can be\nwrong. That is uncomfortable and it is the point: the computed answer can be\ncompared against what the SDK actually does on the same inputs, and any\ndivergence between the two becomes visible immediately rather than during an\noutage. The evaluation explainer shows both answers side by side for exactly\nthis reason.",[39,11916,11917],{},"It costs something real. Computing an audience over a large user base is\nexpensive and sometimes only approximate, so the figure carries its freshness\nand a visible marker when it is an estimate. An approximate number presented\nwith confidence would be worse than the rule list it replaced — trust in a\nconsole of this kind is binary, and one confidently wrong answer ends it\npermanently.",[367,11919,11921],{"id":11920},"the-kill-switch-argument","The kill switch argument",[39,11923,11924],{},"The most contested decision here is removing the confirmation dialog, so the\nreasoning is worth setting out fully.",[39,11926,11927],{},"There are two conventional designs. A guarded kill switch puts a modal in front\nof the action; an unguarded one does not. The guarded version costs seconds in\nthe one flow where seconds are the entire value of the product, and it prevents\nnothing, because a dialog in a hot path is clicked through unread by everyone,\nevery time. The unguarded version gets hit by accident.",[39,11929,11930,11931,11934],{},"Both designs assume the failure is ",[947,11932,11933],{},"pressing the button unintentionally",". That is\nnot the common failure. The common failure is pressing it intentionally without\nknowing what it covers — killing a flag that turns out to be attached to three\nother services, at 3am, while trying to help.",[39,11936,11937],{},"So the design addresses that instead. The blast radius is stated beside the\ncontrol, before the press: which services, which environments, how many users.\nThe action is reversible with one click for five minutes. Every kill writes an\naudit entry naming the person, which matters more for the follow-up than for the\nprevention.",[39,11939,11940],{},"This is a risk transfer, not a risk elimination, and it should be said plainly.\nAccidental kills will happen and will briefly affect production. A team whose\nrollback path is slow, or whose flags routinely have wide blast radii, should\nkeep the dialog — the trade only works if reversal is genuinely instant.",[367,11942,11944],{"id":11943},"flag-debt-is-a-design-problem","Flag debt is a design problem",[39,11946,11947],{},"The hygiene work is the least glamorous part of this and possibly the most\nvaluable.",[39,11949,11950],{},"Every flag is created as temporary. A large share become permanent, and each\nsurvivor is a live conditional in production that nobody remembers the reason\nfor. The usual remedy is a platform engineer with a spreadsheet and persistence,\nwhich works exactly as well as one person's persistence.",[39,11952,11953],{},"Mandatory expiry makes the debt a state the tool displays rather than something\nsomeone tracks. But the expiry date alone would only produce a queue of\nnotifications, and notifications about other people's cleanup are the easiest\nthing in the world to ignore. What makes it work is generating the removal pull\nrequest once a flag's evaluation has been constant long enough to be safe —\nbecause the obstacle was never willingness. It was that removing a flag means\nfinding every call site, and nobody wants to do that on a Thursday.",[39,11955,11956],{},"The part that does not work is expiry churn. Rollouts legitimately run long,\nextensions become reflexive, and by the third snooze the date means nothing.\nRequiring a reason and showing extensions in the hygiene view slows the reflex a\nlittle. It does not fix it, and this design does not have a good answer.",{"title":46,"searchDepth":47,"depth":47,"links":11958},[11959,11960,11961,11962],{"id":11888,"depth":203,"text":11889},{"id":11904,"depth":203,"text":11905},{"id":11920,"depth":203,"text":11921},{"id":11943,"depth":203,"text":11944},{"summary":11964,"points":11965},"Halyard is a scenario about a category of tool that is powerful, widely adopted, and consistently unhelpful at the exact moment it matters most. Feature-flag platforms are excellent at storing rules. During an incident nobody wants to read rules — they want an answer, and deriving the answer from four separate rule surfaces under pressure is where these tools fail.",[11966,11969,11972,11975,11978,11981],{"label":11967,"detail":11968},"The state is distributed across four surfaces","A flag's effect is the product of its default, its environment overrides, its cohort targeting and its individual overrides. Each is stored and displayed separately, and the reader has to combine them mentally to reach the only answer they came for.",{"label":11970,"detail":11971},"The critical question is asked under the worst conditions","\"Who is seeing this?\" is almost always asked at 3am, by someone who did not configure the rollout, while a graph is going the wrong way. Any interface requiring careful reading has failed at that moment.",{"label":11973,"detail":11974},"The kill switch is either slow or dangerous","Guarded by a confirmation dialog it costs seconds nobody has, and the dialog is clicked through unread anyway. Unguarded it gets hit by accident. Both designs are common and both are wrong.",{"label":11976,"detail":11977},"Temporary flags are permanent","A flag created for a two-week rollout is still evaluated three years later, and every one is a live branch in production code. Nothing in the tool makes that accumulation visible.",{"label":11979,"detail":11980},"Staging and production look identical","The same console, the same layout, one small dropdown between them. The resulting mistake is rare, severe, and entirely predictable from the design.",{"label":11982,"detail":11983},"The SDK and the console must agree","If the console says a user is in the rollout and the SDK disagrees, the console is worse than useless — it is confidently wrong, and it will be trusted during an incident.","Halyard","\u002Fcase-studies\u002Fhalyard\u002Fscreens\u002Fflag-surface.webp","Halyard flag surface leading with the computed audience",[11988,11994,12000,12006,12012],{"title":11989,"problem":11990,"decision":11991,"rationale":11992,"tradeoff":11993},"Compute the audience instead of displaying the rules","Flags store a default, environment overrides, cohort targeting and individual overrides. Every one of those is displayed faithfully, and the answer the reader wants is in none of them — it is the product of all four, computed in the reader's head under time pressure.","The flag surface leads with the computed audience — who is currently affected, by segment, with a count. Rules move one level down, where the person editing them will still find them.","It moves the computation from the reader to the machine, which is the whole job. It also makes the console checkable: a computed answer can be compared against the SDK's evaluation, whereas a list of rules can only be agreed with.","Computing an audience over a large user base is expensive and sometimes approximate, so the figure carries its own freshness and a clear marker when it is an estimate. An approximate answer presented confidently would be worse than the rule list it replaced.",{"title":11995,"problem":11996,"decision":11997,"rationale":11998,"tradeoff":11999},"A kill switch with no confirmation dialog","The two conventional designs both fail. A confirmation dialog costs seconds during an incident and is clicked through unread, so it prevents nothing while slowing the one action that matters. No guard at all means accidental kills.","One action, no modal. Safety comes from three other places: the blast radius is stated beside the control before it is pressed, the action is reversible with one click for a sustained window, and every kill writes an audit entry naming the person.","Dialogs defend against the wrong failure. The real risk is not that someone presses the button without meaning to — it is that they press it not knowing what it covers. Stating the scope addresses that; instant reversibility handles the rest.","Accidental kills will happen and will briefly affect production. We judged a rare, immediately reversible accident better than seconds added to every genuine incident, but this is a real risk transfer rather than a risk elimination, and a team with a slow rollback path should not accept it.",{"title":12001,"problem":12002,"decision":12003,"rationale":12004,"tradeoff":12005},"Mandatory expiry at flag creation","Every flag is created as temporary and a large share become permanent. Each survivor is a live conditional in production that nobody remembers the reason for, and no team voluntarily audits them.","Creation requires an expiry date. Past it the flag enters an expiring state, appears in the hygiene view with its owner, and — once its evaluation has been constant for long enough — generates a removal pull request.","It converts flag debt from an invisible accumulation into a state the tool displays, and the generated pull request removes the actual obstacle, which was never willingness but the tedium of finding every call site.","Expiry churn. Rollouts legitimately run long, and every extension is a small tax that becomes a reflex — after the third snooze the date is meaningless. Extensions therefore require a reason and are shown in the hygiene view, which helps a little and does not solve it.",{"title":12007,"problem":12008,"decision":12009,"rationale":12010,"tradeoff":12011},"Production carries persistent chrome, not a dropdown","Staging and production share a console, a layout and a small environment selector. The resulting mistake is rare and severe, and it is entirely predictable from the design.","Production gets a persistent bordered frame, a distinct accent, the word PRODUCTION in the header, and the environment name inside every destructive control's label rather than only in the selector.","The signal needs to be present in peripheral vision at the moment of action, not available for checking beforehand. Putting the environment in the button label means the last thing read before clicking states which environment is about to change.","Persistent chrome costs screen space on every production view, which is most views, and the effect fades with familiarity as all such warnings do. Putting the environment in the control label is the part that keeps working after the frame becomes invisible.",{"title":12013,"problem":12014,"decision":12015,"rationale":12016,"tradeoff":12017},"An evaluation explainer as a first-class surface","\"Is this specific user affected?\" cannot be answered from rule definitions without simulating the evaluation by hand, which is why incident tickets end up containing screenshots of SDK debug output.","Paste an identifier and see the exact rule path — every rule considered in order, which one short-circuited, and the resulting variant, alongside the SDK's answer on the same inputs.","It answers the question directly and, by showing both answers together, makes any console-versus-SDK divergence immediately visible rather than discovered during an outage.","It exposes internal evaluation order, which constrains future changes to it — people will build habits around what they see. It is also a surface that handles user identifiers, so it needs access control and audit that the rest of the console does not.","A concept study in developer tooling — a feature-flag and rollout platform designed around the one question nobody can answer during an incident, plus a kill switch that is fast without being accidental.",[12020,12024,12027,12031,12034,12037],{"title":12021,"detail":12022,"icon":12023},"Computed audience panel","One authoritative answer to who is currently affected, with a segment breakdown, its own freshness and an explicit marker when it is an estimate.","lucide:users",{"title":12025,"detail":12026,"icon":141},"Evaluation explainer","Any identifier evaluated against live rules, showing the ordered rule path and the SDK's answer on the same inputs.",{"title":12028,"detail":12029,"icon":12030},"Reversible scoped kill","Blast radius stated before the action, no confirmation dialog, one-click reversal for five minutes, and an audit entry naming the actor.","lucide:zap",{"title":12032,"detail":12033,"icon":9545},"Enforced flag lifecycle","Mandatory expiry at creation, six lifecycle states, and generated removal pull requests once evaluation has been constant.",{"title":12035,"detail":12036,"icon":10579},"Unmistakable environments","Persistent production chrome plus the environment name inside every destructive control label, so the signal survives familiarity.",{"title":12038,"detail":12039,"icon":11061},"Change-first landing view","Recent rollout changes across all flags, correlated with service error rates, because recency is what an incident asks about.",[12041,12052,12062,12074],{"title":12042,"note":12043,"before":12044,"after":12051},"Answering \"who is seeing this?\"","The flow the entire product is judged on. The before column is not badly designed — each of those screens is fine on its own. The failure is that the answer exists in none of them.",[12045,12046,12047,12048,12049,12050],"Open the flag","Read the default","Open environment overrides","Open cohort targeting","Open individual overrides","Combine mentally",[12045],{"title":12053,"note":12054,"before":12055,"after":12060},"Checking whether a specific user is affected","Impossible in the current state, which is why incident tickets contain screenshots of SDK debug logs.",[12056,12057,12058,12059],"Read the cohort definitions","Work out whether the user matches","Ask the owning team","Read SDK logs",[12061],"Paste the identifier into Explain",{"title":12063,"note":12064,"before":12065,"after":12071},"Stopping a bad rollout","The blast radius being stated before the action is what allows the scoped kill to be chosen over the blunt one.",[12066,12067,12068,12069,12070],"Find the flag","Guess the blast radius","Click disable","Dismiss a confirmation dialog","Hope nothing unrelated was attached",[12072,12073],"Read the stated blast radius","Kill, scoped",{"title":12075,"note":12076,"before":12077,"after":12082},"Retiring a finished flag","Currently driven entirely by one platform engineer with a spreadsheet and persistence.",[12078,12079,12080,12081],"Maintain a spreadsheet","Chase the owning team","Wait","Chase again",[12083,12084],"Flag enters the expiring state automatically","Accept the generated removal pull request",[12086,12090,12096,12100,12106,12111],{"title":12087,"caption":12088,"src":11985,"width":11111,"alt":12089,"frame":9604,"span":9605},"Flag surface","The computed audience is the primary content; the rules that produce it are one level down. A console that lists rules is unfalsifiable — one that computes an answer is making a claim that can be checked.","Feature flag console showing a computed audience of 42,908 users",{"title":12091,"caption":12092,"src":12093,"width":12094,"alt":12095,"frame":9622,"span":9605},"The answer, not the rules","Who is affected, by segment, with the computation's own age and an explicit marker where the figure is sampled rather than exact.","\u002Fcase-studies\u002Fhalyard\u002Fcomponents\u002Fflag-surface--audience.webp",1372,"Computed audience panel with segment breakdown and freshness",{"title":12025,"caption":12097,"src":12098,"width":11111,"alt":12099,"frame":9604},"An identifier in, the exact rule path out — including which rule short-circuited — beside the SDK's answer on identical inputs.","\u002Fcase-studies\u002Fhalyard\u002Fscreens\u002Fexplain.webp","Rule path trace for a single user identifier",{"title":12101,"caption":12102,"src":12103,"width":12104,"alt":12105,"frame":9622},"Console against SDK","Showing both answers together makes divergence visible here rather than during an outage. Trust in a console of this kind is binary.","\u002Fcase-studies\u002Fhalyard\u002Fcomponents\u002Fexplain--sdk.webp",543,"Console and SDK evaluation results shown side by side",{"title":12107,"caption":12108,"src":12109,"width":11111,"alt":12110,"frame":9604,"span":9605},"Kill control","Blast radius stated before the press, no confirmation dialog, one-click reversal for five minutes. The reasoning is printed on the screen for whoever later proposes adding the dialog back.","\u002Fcase-studies\u002Fhalyard\u002Fscreens\u002Fkill.webp","Kill control showing blast radius and a five-minute undo window",{"title":12112,"caption":12113,"src":12114,"width":12115,"alt":12116,"frame":9622},"Production is unmistakable","Persistent frame, distinct accent, the word in the header — and the environment name inside the destructive control's own label, so the last text read before the click says what is about to change.","\u002Fcase-studies\u002Fhalyard\u002Fcomponents\u002Fkill--kill.webp",726,"Destructive control labelled with the production environment",[12118,12128,12143,12153],{"label":12119,"detail":12120,"children":12121},"Changes","The landing surface. Ordered by recency, because that is the incident question.",[12122,12125],{"label":12123,"detail":12124},"Recent changes","Every rollout state change in the last 24 hours, across all flags",{"label":12126,"detail":12127},"Correlation","Change timeline overlaid on service error rates",{"label":12129,"detail":12130,"children":12131},"Flag","One surface holding the computed audience, the lifecycle state and the controls.",[12132,12135,12138,12140],{"label":12133,"detail":12134},"Audience","The computed answer — who is currently affected, and how many",{"label":12136,"detail":12137},"Lifecycle","One of six states, with the expiry date and its owner",{"label":10689,"detail":12139},"Default, environment, cohort and individual overrides, one level down",{"label":12141,"detail":12142},"Kill","Scoped disable with the blast radius stated before the action",{"label":12144,"detail":12145,"children":12146},"Explain","Evaluate any identifier against live rules and show the path taken.",[12147,12150],{"label":12148,"detail":12149},"Rule path","Every rule considered, in order, and which one short-circuited",{"label":12151,"detail":12152},"SDK parity","The console's answer checked against the SDK's on identical inputs",{"label":12154,"detail":12155,"children":12156},"Hygiene","Flag debt as a visible state rather than a spreadsheet somebody maintains.",[12157,12160,12163],{"label":12158,"detail":12159},"Expiring","Flags past or approaching their declared expiry, with owners",{"label":12161,"detail":12162},"Dead","Fully rolled out or fully off for a sustained period",{"label":12164,"detail":12165},"Removal pull requests","Generated for flags whose evaluation is now constant","Developer Tools",{},[12169,12172,12176,12179,12182,12185],{"value":9705,"label":12170,"description":12171},"Authoritative answer to \"who sees this\"","A computed audience, replacing four rule surfaces a reader had to combine mentally to reach an answer.",{"value":12173,"label":12174,"description":12175},"5 → 1","Screens to diagnose a rollout","Flag state, environment, cohort membership, overrides and evaluation history consolidated onto one surface.",{"value":1006,"label":12177,"description":12178},"Confirmation dialogs on the kill switch","One action, no modal. Safety comes from scope, reversibility and an audit entry, not from a dialog nobody reads at 3am.",{"value":10211,"label":12180,"description":12181},"Flags carrying an expiry","Creation requires one. A flag without an expiry is permanent branching disguised as a temporary experiment.",{"value":10712,"label":12183,"description":12184},"Lifecycle states specified","Draft, rolling, held, complete, expiring and archived — making flag debt visible as a state rather than a spreadsheet.",{"value":9705,"label":12186,"description":12187},"Action to explain any evaluation","Paste an identifier, get the exact rule path that produced that user's variant, including which rule short-circuited.","\u002Fwork\u002Fhalyard-progressive-delivery",[12190,12191,12192],"Web console","CLI","SDK surface",[12194,12197,12200,12203,12206,12209],{"title":12195,"detail":12196},"Show the answer, not the rules that imply it","The console computes the current audience and presents it as the primary content. Rules remain available, one level down, for the person changing them rather than the person reading them.",{"title":12198,"detail":12199},"Design for the reader who did not write it","Every surface is built for someone encountering this flag for the first time, at speed, under stress. The author is the easier case and gets the second-priority treatment.",{"title":12201,"detail":12202},"Safety through reversibility, not friction","A confirmation dialog in a hot path is a speed bump that gets clicked through unread. Making the action instantly and obviously undoable is what actually prevents harm.",{"title":12204,"detail":12205},"Every flag has an expiry","Set at creation, no exceptions. A permanent flag is a branch in production code, and it should have to be named as one.",{"title":12207,"detail":12208},"Environments are never confusable","Production carries persistent chrome, a distinct colour and an explicit word. Colour alone is insufficient and is never used alone.",{"title":12210,"detail":12211},"The console must be provably right","Its computed answer is checked against the SDK's evaluation on the same inputs. A console that is confidently wrong once is never trusted again.","Developer platform design & information architecture",{"summary":12214,"premises":12215,"personas":12234,"journey":12261},"The premises focus on the incident path rather than the configuration path, because configuration is done calmly by someone who understands the system and diagnosis is done urgently by someone who does not. Tools in this category are almost always designed for the first case.",[12216,12219,12222,12225,12228,12231],{"label":12217,"detail":12218},"The reader is not the author","The person diagnosing a rollout at 3am did not set it up and has no context on why the cohorts are shaped as they are. Every surface must be readable cold.",{"label":12220,"detail":12221},"Rules are stored; answers are wanted","The gap between \"here are four rule sets\" and \"here is who is affected\" is small for a calm expert and enormous under pressure.",{"label":12223,"detail":12224},"Confirmation dialogs do not prevent mistakes","We assume any dialog in a hot path is clicked through unread. Safety has to come from reversibility and scope rather than from a speed bump.",{"label":12226,"detail":12227},"Flag debt accumulates silently","We assume no team voluntarily audits flags. If cleanup is not surfaced as state in the tool, it does not happen.",{"label":12229,"detail":12230},"Environment mistakes are a design failure","We assume that if staging and production are visually identical, someone will eventually act on the wrong one, and the design owns that outcome.",{"label":12232,"detail":12233},"Trust in the console is binary","One occasion where the console disagreed with observed behaviour, and engineers go back to reading the SDK logs permanently.",[12235,12248],{"name":12236,"role":12237,"context":12238,"goals":12239,"frustrations":12243,"quote":12247},"Sam Okafor","On-call backend engineer","Paged at 3am about an error rate on a service they do not own. They suspect a rollout, have never opened this flag before, and need to know within a minute whether to kill it.",[12240,12241,12242],"Find out who is currently affected, immediately","Stop the bleeding without taking down anything unrelated","Leave a record so the owning team knows what was done",[12244,12245,12246],"Four screens to combine before an answer exists","No way to check a specific reported user against the rules","A kill switch guarded by a dialog, or one that kills more than intended","I do not care how the targeting is configured. I need to know who is getting the bad code and how to make that stop.",{"name":12249,"role":12250,"context":12251,"goals":12252,"frustrations":12256,"quote":12260},"Priya Venkatesan","Platform engineer, developer experience","Owns the flag system across forty services. Spends a recurring part of every quarter chasing teams to remove flags that finished rolling out long ago and are now permanent conditionals.",[12253,12254,12255],"See which flags are dead without auditing by hand","Give teams a reason to clean up that is not her asking","Keep the console and the SDK provably in agreement",[12257,12258,12259],"No lifecycle state, so a finished flag looks like an active one","Cleanup is a spreadsheet she maintains personally","No way to prove the console's answer matches the SDK's","Every flag nobody removed is an `if` statement in production that somebody will trip over in two years.",[12262,12265,12270,12272,12277,12282],{"stage":12263,"doing":12264,"feeling":9796},"Page fires","Wakes up, opens the alert, sees an error rate climbing on one service.",{"stage":12266,"doing":12267,"feeling":9792,"friction":12268,"opportunity":12269},"Suspect a rollout","Opens the flag console and searches for recent changes.","Recency is not a first-class view; changes are buried in per-flag history.","A changes-first landing view scoped to the last few hours.",{"stage":12066,"doing":12271,"feeling":9792},"Locates a flag that changed forty minutes before the alert.",{"stage":12273,"doing":12274,"feeling":9796,"friction":12275,"opportunity":12276},"Work out the audience","Reads the default, then environment overrides, then cohorts, then individual overrides.","Four surfaces to combine mentally, at 3am, on someone else's configuration.","Compute the audience and show it as the primary content.",{"stage":12278,"doing":12279,"feeling":9796,"friction":12280,"opportunity":12281},"Check a reported user","Tries to confirm whether the one user in the ticket is actually affected.","No way to evaluate a specific identifier against the live rules.","An evaluation explainer that shows the exact rule path taken.",{"stage":12283,"doing":12284,"feeling":9796,"friction":12285,"opportunity":12286},"Kill it","Hesitates over the scope of the kill switch, then disables the whole flag.","Unclear blast radius, so the safe move is the blunt one.","Scoped kill with the blast radius stated, instantly reversible.",{"title":11881,"description":12018},[8182,9820,9821],[10335,9824,9825,12290,12291],"Go","OpenTelemetry","work\u002F6.halyard-progressive-delivery",{"summary":12294,"radius":290,"density":11315,"palette":12295,"type":12306,"tokens":12322,"components":12344,"specimens":12363},"A dense console system tuned for reading under stress. Tight radii and compact density, monospace for anything an engineer will copy, a status ramp held apart from the categorical chart hues, and environment identity treated as a system-level concern rather than a page-level one.",[12296,12299,12300,12302,12303],{"name":12297,"value":12298,"role":10346},"Terminal","#0B0B10",{"name":11984,"value":11882,"role":11322},{"name":12301,"value":11883,"role":10352},"Halyard (light)",{"name":11326,"value":9845,"role":11327},{"name":12304,"value":11330,"role":12305},"Production","Environment identity only. Never a series colour, never a status.",[12307,12310,12312,12314,12317],{"name":12308,"size":10362,"weight":9855,"tracking":9861,"usage":12309},"Count","Audience sizes and affected-user counts, tabular",{"name":10853,"size":10368,"weight":9855,"usage":12311},"Flag names and panel titles",{"name":9864,"size":11339,"weight":9866,"usage":12313},"Descriptions, rule text and console copy",{"name":12315,"size":9870,"weight":9866,"usage":12316},"Code","Flag keys, identifiers and rule paths — anything that gets copied",{"name":12318,"size":12319,"weight":9855,"tracking":12320,"usage":12321},"Chrome","0.6875rem","0.14em","PRODUCTION — environment identity in the persistent frame",[12323,12327,12331,12335,12338,12341],{"name":12324,"value":12325,"usage":12326},"--env-chrome","persistent","Production identity is frame, colour and word. Never colour alone.",{"name":12328,"value":12329,"usage":12330},"--destructive-label","includes environment","The last text read before a destructive click names the environment.",{"name":12332,"value":12333,"usage":12334},"--undo-window","5min","One-click reversal on any kill. Replaces the confirmation dialog.",{"name":12336,"value":8449,"usage":12337},"--audience-freshness","Every computed audience states its age and whether it is an estimate.",{"name":12339,"value":8449,"usage":12340},"--expiry","Enforced at creation. There is no path to a flag without one.",{"name":12342,"value":11350,"usage":12343},"--row-height","Compact. A console is read in bulk, not admired.",[12345,12348,12351,12354,12357,12360],{"name":12346,"variants":296,"detail":12347},"AudiencePanel","The computed answer, with segment breakdown, freshness and estimate marker.",{"name":12349,"variants":290,"detail":12350},"KillControl","Scoped disable stating its blast radius, with a five-minute one-click undo.",{"name":12352,"variants":306,"detail":12353},"LifecycleBadge","Draft, rolling, held, complete, expiring, archived — icon and word, never colour alone.",{"name":12355,"variants":315,"detail":12356},"RulePath","Ordered evaluation trace showing which rule short-circuited and why.",{"name":12358,"variants":47,"detail":12359},"EnvironmentFrame","Persistent chrome carrying environment identity across every view.",{"name":12361,"variants":306,"detail":12362},"ChangeRow","A rollout state change with actor, time and correlated service impact.",[9922,1278,10421,9920],"RRzOGqFfee5MtsHHwKuvTI-U2zKZmoZxf5hSPSt_k1w",[12366,12529,12690],{"id":12367,"title":12368,"body":12369,"description":12522,"extension":51,"meta":12523,"navigation":24,"path":12524,"seo":12525,"stem":12526,"updated":12527,"__hash__":12528},"legal\u002Flegal\u002Faccessibility.md","Accessibility Statement",{"type":36,"value":12370,"toc":12515},[12371,12374,12378,12388,12392,12402,12412,12421,12431,12444,12450,12456,12462,12466,12480,12484,12498,12501,12505],[39,12372,12373],{},"We build accessible products for clients, so this site is held to the same standard.",[367,12375,12377],{"id":12376},"conformance","Conformance",[39,12379,12380,12383,12384,12387],{},[517,12381,12382],{},"uxatom.com"," targets ",[517,12385,12386],{},"WCAG 2.2 Level AA",". We believe it currently meets that standard, with the known exceptions listed below.",[367,12389,12391],{"id":12390},"what-we-have-done","What we have done",[39,12393,12394,12397,12398,12401],{},[517,12395,12396],{},"Semantic structure."," One ",[247,12399,12400],{},"\u003Ch1>"," per page, headings in order, landmark regions, and lists marked up as lists. Every page starts with a skip link to the main content.",[39,12403,12404,12407,12408,12411],{},[517,12405,12406],{},"Keyboard."," Every interactive element is reachable and operable by keyboard. Menus, dialogs and the mobile navigation trap focus while open, close on ",[247,12409,12410],{},"Escape",", and return focus to the control that opened them. Focus indicators are visible against every background we use.",[39,12413,12414,12417,12418,1886],{},[517,12415,12416],{},"Contrast."," Body and interface text meets 4.5:1 against its background; large text and non-text indicators meet 3:1. A higher-contrast variant is applied automatically when the OS reports ",[247,12419,12420],{},"prefers-contrast: more",[39,12422,12423,12426,12427,12430],{},[517,12424,12425],{},"Motion."," All animation is decorative and is disabled entirely under ",[247,12428,12429],{},"prefers-reduced-motion: reduce",". No content is revealed only through animation — scroll-triggered content is present and readable without it.",[39,12432,12433,12436,12437,12440,12441,12443],{},[517,12434,12435],{},"Forms."," Every input has a persistent visible label, errors are announced via ",[247,12438,12439],{},"role=\"alert\""," and linked with ",[247,12442,8656],{},", and no field relies on a placeholder as its label.",[39,12445,12446,12449],{},[517,12447,12448],{},"Colour."," No information is conveyed by colour alone. Status indicators pair colour with a shape and a text label.",[39,12451,12452,12455],{},[517,12453,12454],{},"Zoom and reflow."," Content reflows without horizontal scrolling to 320px width, and remains usable at 200% zoom. Wide elements such as tables and code blocks scroll within their own container rather than the page.",[39,12457,12458,12461],{},[517,12459,12460],{},"Themes."," Both dark and light themes are tested for contrast. The theme respects your OS preference and can be overridden.",[367,12463,12465],{"id":12464},"how-we-test","How we test",[2038,12467,12468,12471,12474,12477],{},[514,12469,12470],{},"Automated checks in CI (axe-core) on every pull request",[514,12472,12473],{},"Manual keyboard-only traversal of each page before release",[514,12475,12476],{},"Screen reader passes with VoiceOver (Safari) and NVDA (Firefox)",[514,12478,12479],{},"Zoom and reflow at 320px and 200%",[367,12481,12483],{"id":12482},"known-issues","Known issues",[2038,12485,12486,12492],{},[514,12487,12488,12491],{},[517,12489,12490],{},"Syntax-highlighted code blocks."," Some token colours in article code samples fall slightly below 4.5:1. We are adjusting the theme; the surrounding prose is unaffected.",[514,12493,12494,12497],{},[517,12495,12496],{},"Marquee logo strip."," The logo cloud animates horizontally. It pauses on hover and stops entirely under reduced motion, but it is not currently pausable via a visible control.",[39,12499,12500],{},"Both are scheduled for the next release.",[367,12502,12504],{"id":12503},"feedback","Feedback",[39,12506,12507,12508,12514],{},"If something on this site is difficult or impossible to use, tell us and we will fix it. Email ",[517,12509,12510],{},[471,12511,12513],{"href":12512},"mailto:hello@uxatom.com","hello@uxatom.com"," with the page and what happened. We aim to respond within two business days.",{"title":46,"searchDepth":47,"depth":47,"links":12516},[12517,12518,12519,12520,12521],{"id":12376,"depth":203,"text":12377},{"id":12390,"depth":203,"text":12391},{"id":12464,"depth":203,"text":12465},{"id":12482,"depth":203,"text":12483},{"id":12503,"depth":203,"text":12504},"Our WCAG 2.2 AA commitment for this site, what we have tested, and what is still outstanding.",{},"\u002Flegal\u002Faccessibility",{"title":12368,"description":12522},"legal\u002Faccessibility","2026-06-01","IarCg0yZKkulNMazURIlS6rAWe1cmDGsxa6TIs8U-uE",{"id":12530,"title":12531,"body":12532,"description":12684,"extension":51,"meta":12685,"navigation":24,"path":12686,"seo":12687,"stem":12688,"updated":12527,"__hash__":12689},"legal\u002Flegal\u002Fprivacy.md","Privacy Policy",{"type":36,"value":12533,"toc":12675},[12534,12537,12541,12547,12553,12559,12562,12566,12580,12583,12587,12633,12637,12640,12644,12652,12655,12658,12661,12664,12668],[39,12535,12536],{},"We collect as little as we can get away with. This page describes exactly what that is.",[367,12538,12540],{"id":12539},"what-we-collect","What we collect",[39,12542,12543,12546],{},[517,12544,12545],{},"Information you give us."," When you submit the contact form we receive your name, email address, and anything you write in the message, plus optional company, budget range and service selections. When you subscribe to the newsletter we receive your email address.",[39,12548,12549,12552],{},[517,12550,12551],{},"Analytics."," We use privacy-preserving, cookieless analytics that record page paths, referrer, approximate country and device category. No cookies are set and no cross-site identifier is created.",[39,12554,12555,12558],{},[517,12556,12557],{},"Server logs."," Our hosting provider records IP addresses and user agents for security and rate limiting. These are retained for 30 days.",[39,12560,12561],{},"We do not use advertising trackers, session recording, or third-party marketing pixels.",[367,12563,12565],{"id":12564},"why-we-collect-it","Why we collect it",[2038,12567,12568,12571,12574,12577],{},[514,12569,12570],{},"To reply to your enquiry",[514,12572,12573],{},"To send the newsletter you asked for",[514,12575,12576],{},"To understand which pages are read, in aggregate",[514,12578,12579],{},"To prevent abuse of our forms",[39,12581,12582],{},"Under GDPR, our lawful bases are consent (newsletter) and legitimate interest (replying to enquiries, keeping the site available).",[367,12584,12586],{"id":12585},"how-long-we-keep-it","How long we keep it",[1278,12588,12589,12599],{},[1281,12590,12591],{},[1284,12592,12593,12596],{},[1287,12594,12595],{},"Data",[1287,12597,12598],{},"Retention",[1297,12600,12601,12609,12617,12625],{},[1284,12602,12603,12606],{},[1302,12604,12605],{},"Contact enquiries",[1302,12607,12608],{},"24 months, then deleted",[1284,12610,12611,12614],{},[1302,12612,12613],{},"Newsletter subscription",[1302,12615,12616],{},"Until you unsubscribe",[1284,12618,12619,12622],{},[1302,12620,12621],{},"Analytics",[1302,12623,12624],{},"14 months, aggregated",[1284,12626,12627,12630],{},[1302,12628,12629],{},"Server logs",[1302,12631,12632],{},"30 days",[367,12634,12636],{"id":12635},"who-we-share-it-with","Who we share it with",[39,12638,12639],{},"Our hosting, email and analytics providers act as processors under contract. We do not sell data, and we do not share it with advertisers. Where data is transferred outside the EEA or UK, that transfer relies on Standard Contractual Clauses.",[367,12641,12643],{"id":12642},"your-rights","Your rights",[39,12645,12646,12647,12651],{},"You can ask us to give you a copy of what we hold, correct it, delete it, or stop processing it. Email ",[517,12648,12649],{},[471,12650,12513],{"href":12512}," and we will respond within 30 days. No account or verification hoop is required beyond confirming you control the address in question.",[39,12653,12654],{},"Newsletter emails include a one-click unsubscribe link that works immediately.",[39,12656,12657],{},"If you are in the EEA or UK and are unhappy with our response, you have the right to complain to your national data protection authority.",[367,12659,12119],{"id":12660},"changes",[39,12662,12663],{},"If we change this policy materially we will update the date above and note the change on this page. We do not retroactively apply new terms to data already collected.",[367,12665,12667],{"id":12666},"contact","Contact",[39,12669,12670,12671],{},"Questions about this policy: ",[517,12672,12673],{},[471,12674,12513],{"href":12512},{"title":46,"searchDepth":47,"depth":47,"links":12676},[12677,12678,12679,12680,12681,12682,12683],{"id":12539,"depth":203,"text":12540},{"id":12564,"depth":203,"text":12565},{"id":12585,"depth":203,"text":12586},{"id":12635,"depth":203,"text":12636},{"id":12642,"depth":203,"text":12643},{"id":12660,"depth":203,"text":12119},{"id":12666,"depth":203,"text":12667},"What UXAtom collects, why, and how to have it removed.",{},"\u002Flegal\u002Fprivacy",{"title":12531,"description":12684},"legal\u002Fprivacy","Gb7f0B9tgxrFx-OIjkHvjY1VzVYzcGBsqkeBx1J-IG0",{"id":12691,"title":12692,"body":12693,"description":12777,"extension":51,"meta":12778,"navigation":24,"path":12779,"seo":12780,"stem":12781,"updated":12527,"__hash__":12782},"legal\u002Flegal\u002Fterms.md","Terms of Service",{"type":36,"value":12694,"toc":12767},[12695,12701,12705,12708,12711,12715,12718,12721,12724,12728,12731,12735,12738,12742,12745,12749,12752,12756,12759,12761],[39,12696,12697,12698,12700],{},"These terms cover use of ",[517,12699,12382],{},". Client engagements are governed by a separate signed agreement, and where the two differ, that agreement takes precedence.",[367,12702,12704],{"id":12703},"using-this-site","Using this site",[39,12706,12707],{},"You may read, link to, and share anything published here. You may quote from our articles with attribution and a link to the original.",[39,12709,12710],{},"You may not scrape the site at a rate that degrades it for others, republish articles in full without permission, or present our work as your own.",[367,12712,12714],{"id":12713},"our-content","Our content",[39,12716,12717],{},"Articles, case studies, illustrations and code samples on this site are © UXAtom unless stated otherwise.",[39,12719,12720],{},"Code samples published in articles are provided under the MIT licence — use them in commercial work freely, no attribution required.",[39,12722,12723],{},"Client work shown in case studies is published with permission. Trademarks and product names belong to their owners.",[367,12725,12727],{"id":12726},"accuracy","Accuracy",[39,12729,12730],{},"We write about work we have done and techniques we use. Metrics in case studies are reported as measured by the client at the time. Nothing here is a guarantee of a comparable result on a different product, and nothing on this site is legal, financial or medical advice.",[367,12732,12734],{"id":12733},"enquiries","Enquiries",[39,12736,12737],{},"Submitting the contact form does not create a contract or a client relationship. We treat the contents of enquiries as confidential, and we are happy to sign an NDA before the first call if you would prefer.",[367,12739,12741],{"id":12740},"availability","Availability",[39,12743,12744],{},"We aim to keep the site available but do not guarantee uptime. We may change or remove content without notice.",[367,12746,12748],{"id":12747},"liability","Liability",[39,12750,12751],{},"To the extent permitted by law, we are not liable for indirect or consequential loss arising from use of this site or reliance on its content. Nothing here limits liability for death, personal injury, or fraud.",[367,12753,12755],{"id":12754},"governing-law","Governing law",[39,12757,12758],{},"These terms are governed by the laws of England and Wales, and the courts of England and Wales have exclusive jurisdiction.",[367,12760,12667],{"id":12666},[39,12762,12763],{},[517,12764,12765],{},[471,12766,12513],{"href":12512},{"title":46,"searchDepth":47,"depth":47,"links":12768},[12769,12770,12771,12772,12773,12774,12775,12776],{"id":12703,"depth":203,"text":12704},{"id":12713,"depth":203,"text":12714},{"id":12726,"depth":203,"text":12727},{"id":12733,"depth":203,"text":12734},{"id":12740,"depth":203,"text":12741},{"id":12747,"depth":203,"text":12748},{"id":12754,"depth":203,"text":12755},{"id":12666,"depth":203,"text":12667},"The terms covering use of this website and our published material.",{},"\u002Flegal\u002Fterms",{"title":12692,"description":12777},"legal\u002Fterms","whrRvd7elRLvd__T9_YzZjeEA5TwrDuFo-Vg5CcMWVk",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":12784},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m9 18l6-6l-6-6\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":12786},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Crect width=\"18\" height=\"11\" x=\"3\" y=\"11\" rx=\"2\" ry=\"2\"\u002F>\u003Cpath d=\"M7 11V7a5 5 0 0 1 10 0v4\"\u002F>\u003C\u002Fg>",1787594494062]