[{"data":1,"prerenderedAt":270},["ShallowReactive",2],{"post-color-contrast-people-can-read":3,"blog-posts":14},{"_path":4,"title":5,"description":6,"date":7,"tags":8,"readingTime":12,"body":13},"\u002Fblog\u002Fcolor-contrast-people-can-read\u002F","Color Contrast People Can Actually Read","Quick checks for text and UI colors that stay legible in light mode, dark mode, and on mediocre displays.","2026-07-07",[9,10,11],"accessibility","css","design",5,"# Color Contrast People Can Actually Read\n\nGood contrast is not about passing a checklist once—it is about text that stays readable on real screens, in sunlight, and when someone bumps their OS into high-contrast mode.\n\n## Start with body text, not the hero gradient\n\nPick foreground and background pairs for paragraphs and buttons first. Decorative gradients can be looser; anything people read for more than a second should hit WCAG AA at minimum (4.5:1 for normal text, 3:1 for large text).\n\nBrowser DevTools and Figma both include contrast checkers. Use them on the actual font weight and size you ship, not a generic sample. Thin gray text at 14px on white may “look elegant” in a mock and fail hard on a dim laptop.\n\nPractical order of decisions:\n\n1. **Page background and body text** — the pair users spend minutes with\n2. **Primary buttons** — label vs fill, and fill vs surrounding page\n3. **Links in body copy** — must be distinguishable from surrounding text\n4. **Secondary chrome** — nav, captions, placeholders (placeholders often fail; do not use them as the only label)\n5. **Decorative layers** — gradients, mesh, blur—behind text only if the text still passes on the effective background\n\n```css\n:root {\n  --bg: #f6f4ef;\n  --text: #1c1917;\n  --muted: #57534e; \u002F* check muted against --bg; “muted” is not an excuse to fail *\u002F\n  --accent: #0f766e;\n  --accent-text: #ecfdf8;\n}\n\nbody {\n  background: var(--bg);\n  color: var(--text);\n}\n\na {\n  color: var(--accent);\n}\n\n.button-primary {\n  background: var(--accent);\n  color: var(--accent-text);\n}\n```\n\nStore pairs as tokens, not one-off hex values sprinkled across components. When contrast fails, you fix a token once instead of hunting twenty copies of “almost the same gray.”\n\nLarge text and bold weights change the math—confirm with the real computed size. Icons that carry meaning (error, success) need contrast too, not only the adjacent label.\n\n## Do not rely on color alone\n\nLinks, errors, and selected states need a second signal—underline, icon, weight, or border. That helps color-blind users and anyone on a dim monitor where teal and gray collapse together.\n\nPatterns that hold up:\n\n- **Links** — underline in body text (or a clear weight\u002Ficon treatment that is not color-only)\n- **Errors** — text message plus icon; red border alone is not enough\n- **Charts** — patterns or labels, not only hue series\n- **Required fields** — text (“required”), not only a red asterisk with no legend\n- **Selected tabs** — underline or marker shape in addition to color\n\n```css\n.prose a {\n  color: var(--accent);\n  text-decoration: underline;\n  text-underline-offset: 0.15em;\n}\n\n.field-error {\n  border-color: #b91c1c;\n}\n\n.field-error-message {\n  color: #991b1b;\n}\n```\n\nFocus rings are part of this story. A 2px accent outline with enough contrast against both the control and the page background keeps keyboard users oriented. Do not make focus color-only and low-contrast.\n\n## Test both themes\n\nDark mode is not “invert and hope.” Muted grays that work on white often disappear on near-black backgrounds. Re-check contrast when you toggle `prefers-color-scheme` or a manual theme switch.\n\n```css\n@media (prefers-color-scheme: dark) {\n  :root {\n    --bg: #0c0a09;\n    --text: #f5f5f4;\n    --muted: #a8a29e;\n    --accent: #2dd4bf;\n    --accent-text: #042f2e;\n  }\n}\n```\n\nDark-theme gotchas I keep hitting:\n\n- White text on saturated brand blues that looked fine as light-theme buttons\n- Borders at `#333` on `#111` that vanish, killing card structure\n- Overlay scrims that are too light, so modal text fails against busy backgrounds\n- Shadows used as the only separator—useless when contrast is already low\n\nTest on a real phone outdoors or near a window once. Lab AA passes can still feel weak on a washed-out panel. If you ship a manual theme toggle, test both themes with the same components—nav, post body, forms, and code blocks. Syntax-highlighted code is a frequent dark-mode failure: theme packs assume a specific background.\n\nForced colors \u002F Windows high contrast modes deserve a quick pass. Prefer semantic tokens and system colors where appropriate so UI does not disappear when the OS overrides your palette.\n\n## Build contrast into the workflow\n\nContrast fixed at the end of a project regresses immediately. Bake checks into design and review:\n\n- Define text\u002Fbackground tokens before exploring decorative palettes\n- Run DevTools contrast on new components in PR review\n- Include a light\u002Fdark screenshot pair for UI changes that touch color\n- Avoid “placeholder gray” for anything essential\n\nIf you use a design tool, set styles for Body, Muted, and Inverse text and reuse them. Random hex from a gradient generator is how portfolios ship beautiful, unreadable captions.\n\n## Wrap-up\n\nReadable color is a product decision: check contrast on real UI elements, support non-color cues, and validate every theme you ship. Start with body text and controls, not the hero gradient, and treat tokens as the source of truth. It is one of the highest-impact accessibility fixes you can make in an afternoon—and one of the easiest to keep if you stop treating contrast as a last-mile polish pass.",[15,24,34,36,43,51,58,66,73,80,87,95,102,109,116,124,130,138,147,155,161,168,174,182,190,197,203,209,216,222,228,234,241,248,255,263],{"_path":16,"title":17,"description":18,"date":19,"tags":20,"readingTime":12},"\u002Fblog\u002Fclient-side-tools-that-earn-trust\u002F","Client-Side Tools That Earn Trust","How to design browser tools that feel private by default—local processing, clear data boundaries, and UX that never asks people to guess where their files go.","2026-07-20",[21,22,23],"privacy","frontend","tools",{"_path":25,"title":26,"description":27,"date":28,"tags":29,"readingTime":33},"\u002Fblog\u002Fusb-c-cables-that-actually-matter\u002F","USB-C Cables That Actually Matter","How to buy USB-C cables by wattage and data speed so you stop guessing which cord charges slowly or fails a file transfer.","2026-07-14",[30,31,32],"gadgets","hardware","productivity",4,{"_path":4,"title":5,"description":6,"date":7,"tags":35,"readingTime":12},[9,10,11],{"_path":37,"title":38,"description":39,"date":40,"tags":41,"readingTime":12},"\u002Fblog\u002Fportable-ssds-worth-carrying\u002F","Portable SSDs Worth Carrying","When a pocket SSD beats cloud sync for travel, backups, and large media—and how to pick one that stays fast and durable.","2026-06-26",[30,42,32],"storage",{"_path":44,"title":45,"description":46,"date":47,"tags":48,"readingTime":12},"\u002Fblog\u002Fwireless-earbuds-that-last-the-day\u002F","Wireless Earbuds That Last the Day","Simple charging and fit habits that stretch earbud battery life without babysitting the case every few hours.","2026-06-18",[30,49,50],"audio","habits",{"_path":52,"title":53,"description":54,"date":55,"tags":56,"readingTime":33},"\u002Fblog\u002Fwhen-a-second-monitor-helps\u002F","When a Second Monitor Actually Helps","A practical take on dual screens for coding, writing, and research—plus when a single good display is still the better desk.","2026-06-10",[30,32,57],"workspace",{"_path":59,"title":60,"description":61,"date":62,"tags":63,"readingTime":33},"\u002Fblog\u002Fshort-session-game-picks\u002F","Short-Session Games That Respect Your Evening","Genres and design patterns that fit a 30-minute window without the “one more hour” trap.","2026-06-04",[64,50,65],"gaming","indie",{"_path":67,"title":68,"description":69,"date":70,"tags":71,"readingTime":12},"\u002Fblog\u002Fphone-charging-habits-that-age-better\u002F","Phone Charging Habits That Age Better","Practical charging routines that keep a phone useful longer—without obsessing over the perfect battery percentage.","2026-05-23",[30,72,50],"mobile",{"_path":74,"title":75,"description":76,"date":77,"tags":78,"readingTime":12},"\u002Fblog\u002Ffinishing-side-projects-without-burning-out\u002F","Finishing Side Projects Without Burning Out","A practical playbook for shrinking scope, shipping thin vertical slices, and keeping personal projects fun long enough to actually launch.","2026-05-14",[32,79,50],"side-projects",{"_path":81,"title":82,"description":83,"date":84,"tags":85,"readingTime":12},"\u002Fblog\u002Faccessible-forms-people-finish\u002F","Accessible Forms That People Actually Finish","Practical accessibility checks for labels, errors, and focus so your forms work for more users.","2026-05-07",[9,86,22],"html",{"_path":88,"title":89,"description":90,"date":91,"tags":92,"readingTime":12},"\u002Fblog\u002Fvue-composition-api-patterns\u002F","Practical Vue Composition API Patterns","Reusable patterns for composables, shared state, and cleaner component logic in Vue 3.","2026-04-27",[93,94,22],"vue","javascript",{"_path":96,"title":97,"description":98,"date":99,"tags":100,"readingTime":12},"\u002Fblog\u002Fdesigning-empty-states-that-teach\u002F","Designing Empty States That Teach the Product","Empty screens are not placeholders—they are onboarding. How to write, layout, and sequence first-run UI so people know what to do next.","2026-04-19",[101,22,11],"ux",{"_path":103,"title":104,"description":105,"date":106,"tags":107,"readingTime":12},"\u002Fblog\u002Fwhy-indie-games-keep-winning\u002F","Why Indie Games Keep Winning Attention","How small teams punch above their weight with focused design, personality, and smart scope.","2026-04-13",[64,65,108],"industry",{"_path":110,"title":111,"description":112,"date":113,"tags":114,"readingTime":12},"\u002Fblog\u002Fkeeping-component-props-simple\u002F","Keeping Component Props Simple","When to split a component, when to use slots, and how to avoid prop objects that grow forever.","2026-04-03",[93,22,115],"best-practices",{"_path":117,"title":118,"description":119,"date":120,"tags":121,"readingTime":12},"\u002Fblog\u002Fcalm-frontend-performance\u002F","A Calm Approach to Frontend Performance","Performance work that sticks: measure what users feel, fix the critical path first, and avoid optimization theater on personal and portfolio sites.","2026-03-26",[122,22,123],"performance","web",{"_path":125,"title":126,"description":127,"date":128,"tags":129,"readingTime":12},"\u002Fblog\u002Fcss-layout-without-the-hacks\u002F","CSS Layout Without the Hacks","Modern Flexbox and Grid techniques that replace brittle floats, magic numbers, and overflow tricks.","2026-03-17",[10,22,11],{"_path":131,"title":132,"description":133,"date":134,"tags":135,"readingTime":12},"\u002Fblog\u002Fcloud-gaming-what-gets-in-the-way\u002F","Cloud Gaming: What Still Gets in the Way","Latency, input feel, and library lock-in—why streaming games is impressive and still uneven.","2026-03-10",[64,136,137],"cloud-gaming","tech",{"_path":139,"title":140,"description":141,"date":142,"tags":143,"readingTime":12},"\u002Fblog\u002Fshipping-static-sites-with-nuxt\u002F","Shipping Static Sites with Nuxt","How I build and deploy a Nuxt site to Cloudflare Pages with predictable routes and content.","2026-03-02",[144,145,146],"nuxt","ssg","devops",{"_path":148,"title":149,"description":150,"date":151,"tags":152,"readingTime":12},"\u002Fblog\u002Fdebugging-faster-in-the-browser\u002F","Debugging Faster in the Browser","A short toolkit of DevTools habits—breakpoints, network filters, and DOM inspection—that cut debug time.","2026-02-24",[94,153,154],"devtools","debugging",{"_path":156,"title":157,"description":158,"date":159,"tags":160,"readingTime":12},"\u002Fblog\u002Fsmall-javascript-habits-that-scale\u002F","Small JavaScript Habits That Scale","Everyday habits—naming, early returns, and clearer async flow—that keep front-end codebases maintainable.","2026-02-18",[94,115,22],{"_path":162,"title":163,"description":164,"date":165,"tags":166,"readingTime":33},"\u002Fblog\u002Fhealthier-gaming-routine\u002F","Building a Healthier Gaming Routine","Simple habits for sessions that stay fun—breaks, goals, and knowing when to stop.","2026-02-02",[64,50,167],"wellness",{"_path":169,"title":170,"description":171,"date":172,"tags":173,"readingTime":12},"\u002Fblog\u002Fresponsive-images-without-layout-shift\u002F","Responsive Images Without Layout Shift","Width, height, srcset, and lazy loading patterns that keep pages stable while images load.","2026-01-22",[86,122,22],{"_path":175,"title":176,"description":177,"date":178,"tags":179,"readingTime":12},"\u002Fblog\u002Fgit-habits-for-small-teams\u002F","Git Habits for Solo and Small Teams","Branch naming, commit messages, and PR hygiene that keep history useful without slowing you down.","2026-01-09",[180,181,32],"git","workflow",{"_path":183,"title":184,"description":185,"date":186,"tags":187,"readingTime":12},"\u002Fblog\u002Fwhat-makes-a-game-stream-worth-watching\u002F","What Makes a Game Stream Worth Watching","Commentary, pacing, and community—why some streams click even when the gameplay is messy.","2025-12-14",[64,188,189],"streaming","content",{"_path":191,"title":192,"description":193,"date":194,"tags":195,"readingTime":12},"\u002Fblog\u002Fwhen-to-reach-for-typescript-vue\u002F","When to Reach for TypeScript on a Vue App","A pragmatic take on where TypeScript pays off in Vue—and where plain JS is still fine.","2025-11-30",[196,93,22],"typescript",{"_path":198,"title":199,"description":200,"date":201,"tags":202,"readingTime":12},"\u002Fblog\u002Fenvironment-variables-for-static-sites\u002F","Environment Variables for Static Sites","How to use build-time env vars in Nuxt and other static generators without leaking secrets or breaking CI.","2025-10-05",[144,146,22],{"_path":204,"title":205,"description":206,"date":207,"tags":208,"readingTime":12},"\u002Fblog\u002Fpicking-a-side-project-stack\u002F","Picking a Side Project Stack Without Overthinking It","How to choose tools for a personal project when the goal is shipping, not building the perfect architecture.","2025-09-18",[32,22,115],{"_path":210,"title":211,"description":212,"date":213,"tags":214,"readingTime":33},"\u002Fblog\u002Fco-op-games-for-busy-friends\u002F","Co-op Games That Work When Everyone Is Busy","Async-friendly and drop-in co-op picks for friend groups that rarely share the same free evening.","2025-08-22",[64,215,50],"co-op",{"_path":217,"title":218,"description":219,"date":220,"tags":221,"readingTime":12},"\u002Fblog\u002Fsemantic-html-still-matters\u002F","Semantic HTML Still Matters","Landmarks, headings, and native elements that make pages easier to use, style, and maintain.","2025-07-06",[86,9,22],{"_path":223,"title":224,"description":225,"date":226,"tags":227,"readingTime":12},"\u002Fblog\u002Fgit-without-the-jargon\u002F","Learning Git Without the Jargon","A plain-language mental model for commits, branches, and merges when tutorials assume you already know Git.","2025-05-14",[180,181,32],{"_path":229,"title":230,"description":231,"date":232,"tags":233,"readingTime":12},"\u002Fblog\u002Fwhy-loading-states-matter\u002F","Why Loading States Matter More Than Animations","Skeletons, disabled buttons, and honest feedback beat flashy spinners when data takes time to arrive.","2025-03-28",[101,22,11],{"_path":235,"title":236,"description":237,"date":238,"tags":239,"readingTime":12},"\u002Fblog\u002Fnaming-things-so-future-you-survives\u002F","Naming Things So Future You Survives","Practical rules for naming variables, components, and files so a codebase stays readable months after the clever jokes fade.","2025-03-10",[94,115,240],"maintainability",{"_path":242,"title":243,"description":244,"date":245,"tags":246,"readingTime":12},"\u002Fblog\u002Fsave-systems-that-respect-time\u002F","Save Systems That Respect the Player’s Time","Checkpoints, autosaves, and manual slots are design choices. How thoughtful saving reduces rage-quits without deleting tension from the game.","2025-02-28",[64,247,101],"game-design",{"_path":249,"title":250,"description":251,"date":252,"tags":253,"readingTime":12},"\u002Fblog\u002Fmicrocopy-that-makes-interfaces-human\u002F","Microcopy That Makes Interfaces Feel Human","Button labels, error lines, and helper text are product design. How to write UI words that reduce hesitation without sounding like a chatbot.","2025-01-22",[101,254,11],"writing",{"_path":256,"title":257,"description":258,"date":259,"tags":260,"readingTime":12},"\u002Fblog\u002Freadmes-that-get-projects-running\u002F","READMEs That Get Projects Running","How to write a README that helps someone—including future you—install, run, and understand a project without opening a support chat.","2024-12-18",[261,262,50],"documentation","open-source",{"_path":264,"title":265,"description":266,"date":267,"tags":268,"readingTime":12},"\u002Fblog\u002Fprogressive-enhancement-still-pays-off\u002F","Progressive Enhancement Still Pays Off","Build the core experience in HTML first, then layer CSS and JavaScript so your site stays useful when networks, scripts, or devices misbehave.","2024-11-05",[86,22,269],"architecture",1784620504460]