1<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="canonical" href="https://www.manuelsanchezdev.com/accessibility"><meta name="generator" content="Astro v7.2.6"><link rel="icon" type="image/x-icon" href="/favicon.png"><!-- General Meta Tags --><title>Accessibility | Manuel Sánchez</title><meta name="title" content="Accessibility | Manuel Sánchez"><meta name="description" content="I'm a passionate developer, entrepreneur, and general technology enthusiast settled in Hamburg. My goal is to help others develop their ideas in the most modern, performantic and web accessible way."><meta name="author" content="Manuel Sánchez"><meta name="keywords" content="web development, web design, game development, web accessibility, web performance, manuelsanchezdev, manuelsanchezweb, developers germany"><link rel="alternate" type="application/rss+xml" title="My Blog RSS Feed" href="/rss.xml"><link rel="sitemap" href="/sitemap-index.xml"><!-- Open Graph / Facebook --><meta property="og:title" content="Accessibility | Manuel Sánchez"><meta property="og:description" content="I'm a passionate developer, entrepreneur, and general technology enthusiast settled in Hamburg. My goal is to help others develop their ideas in the most modern, performantic and web accessible way."><meta property="og:url" content="https://www.manuelsanchezdev.com/accessibility"><meta property="og:image" content="https://www.manuelsanchezdev.com/og-manuel.png"><!-- Twitter --><meta property="twitter:card" content="summary_large_image"><meta property="twitter:url" content="https://www.manuelsanchezdev.com/accessibility"><meta property="twitter:title" content="Accessibility | Manuel Sánchez"><meta property="twitter:description" content="I'm a passionate developer, entrepreneur, and general technology enthusiast settled in Hamburg. My goal is to help others develop their ideas in the most modern, performantic and web accessible way."><meta property="twitter:image" content="https://www.manuelsanchezdev.com/og-manuel.png"><meta name="google-site-verification" content="YPcQGxoLwK4NM97XuiAUkN185-fT2n5Gj2g69lw4z1s">
1<script type="module" src="/webcomponents/baseline-status.js"></script>
1<meta name="astro-view-transitions-enabled" content="true"><meta name="astro-view-transitions-fallback" content="animate">
1<script type="module" src="/_astro/ClientRouter.astro_astro_type_script_index_0_lang.7SAn6vA6.js?dpl=6aa91bb25e381f000817a421"></script>
1<link rel="stylesheet" href="/_astro/_astro_transitions.BurhBCJi.css"><link rel="stylesheet" href="/_astro/Layout.B5czA2fj.css">
1<script type="module" src="/_astro/page.BVUqBe9C.js"></script>
1</head><body><div class="skip-links"><a href="#main-content">Skip to main content</a><a href="#footer">Skip to footer</a></div>
1<script type="module">document.addEventListener(`astro:page-load`,()=>{document.querySelectorAll(`.skip-links a`).forEach(e=>{e.addEventListener(`keydown`,e=>{if(!e.target.closest(`a`)||e.key!==`Enter`)return;e.preventDefault();let t=e.target.getAttribute(`href`),n=document.querySelector(t);if(n)n.setAttribute(`tabindex`,`-1`),n.focus();else if(document.querySelector(`h1`)){let e=document.querySelector(`h1`);e.setAttribute(`tabindex`,`-1`),e.focus()}})})});</script>
1<header class="bg-primary-bg text-primary-text h-24 border-b border-slate-200/80"><div class="mx-auto flex h-full w-full max-w-7xl items-center px-5 sm:px-6 lg:px-8"><div class="relative z-50 flex w-full items-center justify-between"><!-- Logo --><div class="flex shrink-0 items-center"><a href="/" title="Go to Home of Manuel Sánchez" class="flex shrink-0 items-center"><span class="font-display text-2xl font-semibold">Manuel Sánchez</span></a></div><!-- Desktop navigation links --><nav aria-label="Main navigation" class="hidden items-center md:flex md:space-x-6 lg:space-x-8"><a href="/accessibility" class="text-primary-text after:bg-primary-text relative duration-200 after:absolute after:-bottom-2.5 after:left-1/2 after:h-0.5 after:w-4 after:-translate-x-1/2 after:rounded-full after:opacity-0 after:content-[""] font-semibold after:opacity-100" data-pathname="/accessibility">Accessibility</a><a href="/blog" class="text-primary-text after:bg-primary-text relative duration-200 after:absolute after:-bottom-2.5 after:left-1/2 after:h-0.5 after:w-4 after:-translate-x-1/2 after:rounded-full after:opacity-0 after:content-[""] font-medium hover:after:opacity-25" data-pathname="/accessibility">Blog</a><a href="/tools" class="text-primary-text after:bg-primary-text relative duration-200 after:absolute after:-bottom-2.5 after:left-1/2 after:h-0.5 after:w-4 after:-translate-x-1/2 after:rounded-full after:opacity-0 after:content-[""] font-medium hover:after:opacity-25" data-pathname="/accessibility">Tools</a><form class="search-form search-bar flex items-center gap-2"><input id="search-input" type="search" name="search" placeholder="Search..." required class="h-fit rounded-md bg-transparent text-xs dark:border-white"><button class="rounded-md border px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800" aria-label="Search term" type="submit"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg></button></form>
1<script type="module">document.addEventListener(`astro:page-load`,()=>{let e=document.querySelector(`.search-form`),t=e.querySelector(`input[type="search"]`);e.addEventListener(`submit`,e=>{e.preventDefault();let n=t.value;n&&(window.location.href=`/search?q=${n}`)})});</script>
1<button aria-label="Toggle Dark Mode" aria-describedby="current-theme" class="themeToggle mx-3! rounded-md border px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800"><svg aria-hidden="true" id="theme-toggle-dark-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"></path></svg><svg aria-hidden="true" id="theme-toggle-light-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"></path></svg></button><span aria-live="polite" id="current-theme" class="current-theme sr-only"></span>
1<script> 2 if (!window.__themeScriptInitialized) { 3 window.__themeScriptInitialized = true 4 5 const getInitialTheme = () => { 6 const saved = localStorage.getItem('theme') 7 if (saved) return saved 8 return window.matchMedia('(prefers-color-scheme: dark)').matches 9 ? 'dark' 10 : 'light' 11 } 12 13 const applyTheme = (theme) => { 14 document.documentElement.classList.toggle('dark', theme === 'dark') 15 localStorage.setItem('theme', theme) 16 } 17 18 const handleToggleClick = () => { 19 const isDark = document.documentElement.classList.toggle('dark') 20 localStorage.setItem('theme', isDark ? 'dark' : 'light') 21 } 22 23 const attachThemeToggleListeners = () => { 24 document.querySelectorAll('.themeToggle').forEach((button) => { 25 if (!button.__themeListenerAttached) { 26 button.__themeListenerAttached = true 27 button.addEventListener('click', handleToggleClick) 28 } 29 }) 30 } 31 32 // INITIAL LOAD 33 window.addEventListener('DOMContentLoaded', () => { 34 applyTheme(getInitialTheme()) 35 attachThemeToggleListeners() 36 }) 37 38 // PAGE SWAP 39 document.addEventListener('astro:after-swap', () => { 40 applyTheme(localStorage.getItem('theme') || 'light') 41 attachThemeToggleListeners() 42 }) 43 } 44</script>
44<style>astro-island,astro-slot,astro-static-slot{display:contents}</style>
44<script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script>
44<script>(()=>{var g=Object.defineProperty;var w=(a,s,c)=>s in a?g(a,s,{enumerable:!0,configurable:!0,writable:!0,value:c}):a[s]=c;var l=(a,s,c)=>w(a,typeof s!="symbol"?s+"":s,c);var E=new Set(["__proto__","constructor","prototype"]);{let a={0:t=>y(t),1:t=>c(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(c(t)),5:t=>new Set(c(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>Number.POSITIVE_INFINITY*t},s=t=>{let[p,e]=t;return p in a?a[p](e):void 0},c=t=>t.map(s),y=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([p,e])=>[p,s(e)]));class f extends HTMLElement{constructor(){super(...arguments);l(this,"Component");l(this,"hydrator");l(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let r=this.querySelectorAll("astro-slot"),n={},d=this.querySelectorAll("template[data-astro-template]");for(let o of d){let i=o.closest(this.tagName);i!=null&&i.isSameNode(this)&&(n[o.getAttribute("data-astro-template")||"default"]=o.innerHTML,o.remove())}for(let o of r){let i=o.closest(this.tagName);i!=null&&i.isSameNode(this)&&(n[o.getAttribute("name")||"default"]=o.innerHTML)}let u;try{u=this.hasAttribute("props")?y(JSON.parse(this.getAttribute("props"))):{}}catch(o){let i=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(i+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${i}`,this.getAttribute("props"),o),o}let h;await this.hydrator(this)(this.Component,u,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});l(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),r.disconnect(),this.childrenConnectedCallback()},r=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});r.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}getRetryImportUrl(e){let r=new URL(e,document.baseURI);return r.searchParams.set("astro-retry",Date.now().toString()),r.toString()}async importWithRetry(e){try{return await import(e)}catch(r){return await new Promise(n=>setTimeout(n,1e3)),import(this.getRetryImportUrl(e))}}handleHydrationError(e){let r=this.getAttribute("component-url"),n=new CustomEvent("astro:hydration-error",{cancelable:!0,bubbles:!0,composed:!0,detail:{error:e,componentUrl:r}});this.dispatchEvent(n)&&console.error(`[astro-island] Error hydrating ${r}`,e)}async start(){let e=JSON.parse(this.getAttribute("opts")),r=this.getAttribute("client");if(Astro[r]===void 0){window.addEventListener(`astro:${r}`,()=>this.start(),{once:!0});return}try{await Astro[r](async()=>{let n=this.getAttribute("renderer-url");try{let[d,{default:u}]=await Promise.all([this.importWithRetry(this.getAttribute("component-url")),n?this.importWithRetry(n):Promise.resolve({default:()=>()=>{}})]),h=this.getAttribute("component-export")||"default";if(h.includes(".")){this.Component=d;for(let m of h.split(".")){if(E.has(m)||!this.Component||typeof this.Component!="object"&&typeof this.Component!="function"||!Object.hasOwn(this.Component,m))throw new Error(`Invalid component export path: ${h}`);this.Component=this.Component[m]}}else{if(E.has(h))throw new Error(`Invalid component export path: ${h}`);this.Component=d[h]}return this.hydrator=u,this.hydrate}catch(d){return this.handleHydrationError(d),()=>{}}},e,this)}catch(n){this.handleHydrationError(n)}}attributeChangedCallback(){this.hydrate()}}l(f,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",f)}})();</script>
44<astro-island uid="Z1LVhki" component-url="/_astro/Notifications.B5XKA4fW.js?dpl=6aa91bb25e381f000817a421" component-export="default" renderer-url="/_astro/client.Ca1pjHMH.js?dpl=6aa91bb25e381f000817a421" props="{}" ssr client="only" opts="{"name":"Notifications","value":true}"></astro-island><!-- Template pages dropdown container 45 <div class="relative" x-data="{ open: false }"> 46 47 <button 48 type="button" 49 class="group flex items-center font-medium duration-200 ease-in-out" 50 :class="open ? 'text-slate-900' : 'text-slate-700 hover:text-slate-900'" 51 @click="open = true" 52 > 53 <span>Pages</span> 54 55 <Icon 56 name="chevron-down" 57 class="ml-2 h-5 w-5 duration-300" 58 :class="open ? 'rotate-180 text-slate-900' : 'text-slate-600 group-hover:text-slate-800'" 59 /> 60 </button> 61 62 63 <div 64 style="display: none" 65 class="absolute right-0 z-20 mt-3 w-52 space-y-1 rounded-lg bg-white p-2.5 outline-none drop-shadow filter focus:outline-none" 66 x-show.transition="open" 67 @click.away="open = false" 68 > 69 { 70 pages.map((link) => ( 71 <a 72 href={link.href} 73 class:list={[ 74 'block px-5 py-3.5 font-medium', 75 pathname === link.href 76 ? 'bg-slate-100/60 text-slate-900' 77 : 'rounded-md text-slate-700 transition duration-300 ease-in-out hover:bg-slate-50 hover:text-slate-900', 78 ]} 79 > 80 {link.label} 81 </a> 82 )) 83 } 84 </div> 85 </div> 86 --></nav><div class="flex items-center"><!-- Mobile menu button --><div class="ml-4 md:hidden" x-data="{ mobileMenuOpen: false }"><button class="group relative z-50 flex cursor-pointer items-center justify-center rounded-full bg-slate-100/80 p-3 shadow-sm ring-1 shadow-sky-100/50 ring-slate-900/5 transition duration-300 ease-in-out hover:bg-slate-200/60 focus:outline-none md:hidden" aria-label="Toggle Navigation" aria-haspopup="true" aria-controls="mobile-menu" x-bind:aria-expanded="mobileMenuOpen" @click="mobileMenuOpen=!mobileMenuOpen"><span class="relative h-3.5 w-4 transform transition duration-500 ease-in-out"><span class="absolute block h-0.5 rotate-0 transform rounded-full bg-slate-700 opacity-100 transition-all duration-300 ease-in-out group-hover:bg-slate-900" :class="mobileMenuOpen ? 'top-1.5 left-1/2 w-0': 'top-0 left-0 w-full'"></span><span class="absolute top-1.5 left-0 block h-0.5 w-full transform rounded-full bg-slate-700 opacity-100 transition-all duration-300 ease-in-out group-hover:bg-slate-900" :class="mobileMenuOpen ? 'rotate-45': 'rotate-0'"></span><span class="absolute top-1.5 left-0 block h-0.5 w-full transform rounded-full bg-slate-700 opacity-100 transition-all duration-300 ease-in-out group-hover:bg-slate-900" :class="mobileMenuOpen ? '-rotate-45': 'rotate-0'"></span><span class="absolute block h-0.5 rotate-0 transform rounded-full bg-slate-700 opacity-100 transition-all duration-300 ease-in-out group-hover:bg-slate-900" :class="mobileMenuOpen ? 'top-1.5 left-1/2 w-0': 'left-0 top-3 w-full'"></span></span></button><!-- Mobile menu container --><div id="mobile-menu" class="md:hidden"><!-- Background dark overlay when mobile menu is open --><div id="mobile-menu-overlay" x-show="mobileMenuOpen" x-transition:enter="duration-200 ease-out" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="duration-150 ease-in" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="bg-opacity-50 fixed inset-0 z-20 bg-slate-900" style="display: none"></div><!-- Mobile menu popover --><div x-show="mobileMenuOpen" x-transition:enter="duration-300 ease-out" x-transition:enter-start="opacity-0 scale-90" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="duration-200 ease-in" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-90" class="bg-primary-bg absolute inset-x-0 top-full z-30 mt-4 origin-top overflow-hidden rounded-2xl px-6 py-7 shadow-xl ring-1 shadow-sky-100/40 ring-slate-900/5" style="display: none" @click.away="mobileMenuOpen = false"><div><!-- Mobile menu links --><div class="flex flex-col space-y-4"><a href="/accessibility" class="text-primary-text hover:text-primary-text/80 block text-base font-semibold duration-200">Accessibility</a><a href="/blog" class="text-primary-text hover:text-primary-text/80 block text-base font-semibold duration-200">Blog</a><a href="/tools" class="text-primary-text hover:text-primary-text/80 block text-base font-semibold duration-200">Tools</a><div class="pointer-events-auto z-10 ml-auto"><button aria-label="Toggle Dark Mode" aria-describedby="current-theme" class="themeToggle mx-3! rounded-md border px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800"><svg aria-hidden="true" id="theme-toggle-dark-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"></path></svg><svg aria-hidden="true" id="theme-toggle-light-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"></path></svg></button><span aria-live="polite" id="current-theme" class="current-theme sr-only"></span>
86<script> 87 if (!window.__themeScriptInitialized) { 88 window.__themeScriptInitialized = true 89 90 const getInitialTheme = () => { 91 const saved = localStorage.getItem('theme') 92 if (saved) return saved 93 return window.matchMedia('(prefers-color-scheme: dark)').matches 94 ? 'dark' 95 : 'light' 96 } 97 98 const applyTheme = (theme) => { 99 document.documentElement.classList.toggle('dark', theme === 'dark') 100 localStorage.setItem('theme', theme) 101 } 102 103 const handleToggleClick = () => { 104 const isDark = document.documentElement.classList.toggle('dark') 105 localStorage.setItem('theme', isDark ? 'dark' : 'light') 106 } 107 108 const attachThemeToggleListeners = () => { 109 document.querySelectorAll('.themeToggle').forEach((button) => { 110 if (!button.__themeListenerAttached) { 111 button.__themeListenerAttached = true 112 button.addEventListener('click', handleToggleClick) 113 } 114 }) 115 } 116 117 // INITIAL LOAD 118 window.addEventListener('DOMContentLoaded', () => { 119 applyTheme(getInitialTheme()) 120 attachThemeToggleListeners() 121 }) 122 123 // PAGE SWAP 124 document.addEventListener('astro:after-swap', () => { 125 applyTheme(localStorage.getItem('theme') || 'light') 126 attachThemeToggleListeners() 127 }) 128 } 129</script>
129</div><!-- <div class="relative" x-data="{ open: false }"> 130 <button 131 class="group flex w-full items-center gap-2 text-base font-semibold duration-200 ease-in-out" 132 :class="open ? 'text-slate-900' : 'text-slate-700 hover:text-slate-900'" 133 @click="open = !open" 134 > 135 <span>Pages</span> 136 <Icon 137 name="chevron-right" 138 class="h-5 w-5 duration-300" 139 :class="open ? 'rotate-90 text-slate-900' : 'text-slate-700 group-hover:text-slate-900'" 140 /> 141 </button> 142 143 <ul 144 style="display: none" 145 class="z-20 space-y-4 px-3" 146 x-show.transition="open" 147 > 148 { 149 pages.map((link) => ( 150 <li class="mt-4"> 151 <a 152 href={link.href} 153 class="block text-md font-medium text-slate-700 transition duration-200 ease-in-out hover:text-slate-900" 154 > 155 {link.label} 156 </a> 157 </li> 158 )) 159 } 160 </ul> 161 </div> --></div></div></div></div></div></div></div></div></header><section aria-label="Latest post or message" class="border bg-gray-100 flex w-full justify-center border-l-transparent border-r-transparent gap-2 dark:bg-black pt-3 pb-2 text-center text-xs text-primary-text dark:shadow-none md:text-md dark:border-b-white"><span class="text-xs"><strong class="font-display">Latest Post: </strong></span><a class="max-w-[200px] flex truncate underline hover:opacity-75 text-xs sm:max-w-full" href="/blog/founding-a-gmbh-in-germany-steps-and-costs">Founding a GmbH in Germany</a></section><main id="main-content" tabindex="-1"><section class="overflow-hidden bg-white py-16 text-black dark:bg-black dark:text-white"><div class="mx-auto max-w-screen-xl px-5 sm:px-6 lg:px-8"><div class="mx-auto max-w-3xl"><div class="flex flex-col gap-6"><h1 class="font-display text-center text-5xl font-semibold text-slate-900 sm:text-6xl lg:leading-none dark:text-white">Accessibility</h1><div class="text-center" id="statement_generated"><div id="statement_header"><h2 class="mb-8 text-xl">Accessibility Activities for WAS Renewal</h2></div></div><section class="border-b border-b-black"><h2 class="mb-4 text-2xl font-bold">2025</h2><div class="mb-6"><h3 class="mb-2 text-xl font-semibold capitalize">courses</h3><ul class="ml-6 list-disc"><li class="mb-3"><strong>Semantic Structure and Navigation (WCAG 2.1)</strong><br><span class="text-sm italic">2025-07-10</span><span><br>1.75 IAAP CAECs</span><br><a href="/pdf/a11y/2025/202507-semantic-structure-navigation.pdf" download class="text-blue-600 underline dark:text-sky-400">Download proof</a></li><li class="mb-3"><strong>Visual Design & Colors (WCAG 2.1)</strong><br><span class="text-sm italic">2025-07-10</span><span><br>1.00 IAAP CAECs</span><br><a href="/pdf/a11y/2025/202507-visual-design-colors.pdf" download class="text-blue-600 underline dark:text-sky-400">Download proof</a></li><li class="mb-3"><strong>Basic Course On How To Use NVDA by weaaare</strong><br><span class="text-sm italic">2025-05-08</span><span><br>Around 3.5 hours of content</span><br><a href="/pdf/nvda.png" download class="text-blue-600 underline dark:text-sky-400">Download proof</a></li><li class="mb-3"><strong>Fast Track to Accessibility for Web Developers, Part 2: Advanced Techniques</strong><br><span class="text-sm italic">2025-04-07</span><span><br>1.00 IAAP CAECs</span><br><a href="/pdf/a11y/2025/202504-advanced-techniques.pdf" download class="text-blue-600 underline dark:text-sky-400">Download proof</a></li><li class="mb-3"><strong>Web Accessibility Testing: Basic Methods and Tools</strong><br><span class="text-sm italic">2025-02-12</span><span><br>2.00 IAAP CAECs</span><br><a href="/pdf/a11y/2025/202504-advanced-techniques.pdf" download class="text-blue-600 underline dark:text-sky-400">Download proof</a></li><li class="mb-3"><strong>Usability Testing for Accessibility</strong><br><span class="text-sm italic">2025-02-04</span><span><br>0.50 IAAP CAECs</span><br><a href="/pdf/a11y/2025/202504-advanced-techniques.pdf" download class="text-blue-600 underline dark:text-sky-400">Download proof</a></li></ul></div><div class="mb-6"><h3 class="mb-2 text-xl font-semibold capitalize">seminars</h3><ul class="ml-6 list-disc"><li class="mb-3"><strong>
161Seminar on Advanced Web Accessibility Techniques by weaaare</strong><br><span class="text-sm italic">2025-06-13</span><span><br>Joined Susana Pallero to discuss advanced web accessibility techniques with their students and bring on some advanced accessibility techniques, like a right use of ARIA.</span><br><a title="See more about Seminar on Advanced Web Accessibility Techniques by weaaare on a new tab" href="https://weaaare.com/" target="_blank" rel="nofollow noopener" class="text-blue-600 underline dark:text-sky-400">See more</a></li><li class="mb-3"><strong>Accessibility from the Trenches by Somos Edison (Accessibilidad Digital Summit)</strong><br><span class="text-sm italic">2025-04-03</span><span><br>Joined this webinar with some other Spanish-speaking experst on web accessibility to discuss how we deal with a11y in our daily life at work.</span><br><a title="See more about Accessibility from the Trenches by Somos Edison (Accessibilidad Digital Summit) on a new tab" href="https://www.linkedin.com/posts/manusanchez2_accesibilidad-digital-summit-programaci%C3%B3n-activity-7312021617008611328-WBAK?utm_source=share&utm_medium=member_desktop&rcm=ACoAABp4EYgBpPAabjGiB34Q1cexVeDxQp_DdPk" target="_blank" rel="nofollow noopener" class="text-blue-600 underline dark:text-sky-400">See more</a></li></ul></div><div class="mb-6"><h3 class="mb-2 text-xl font-semibold capitalize">talks</h3><ul class="ml-6 list-disc"><li class="mb-3"><strong>Going Beyond Borders (And Outlines): Crafting Inclusive Experiences with Modern CSS</strong><br><span class="text-sm italic">2025-06-04</span><span><br>Talk given at the pre-event of the CSS Day 2025 in Amsterdam.</span><br><a title="See more about Going Beyond Borders (And Outlines): Crafting Inclusive Experiences with Modern CSS on a new tab" href="/pdf/a11y/2025/CSSDay-Accessibility-2025.pdf" target="_blank" rel="nofollow noopener" class="text-blue-600 underline dark:text-sky-400">See more</a></li></ul></div><div class="mb-6"><h3 class="mb-2 text-xl font-semibold capitalize">articles</h3><ul class="ml-6 list-disc"><li class="mb-3"><strong>A11y Considerations in Math on the Web</strong><br><span class="text-sm italic">2025-12-12</span><span><br>Article for HTMHell about MathML, screen reader behavior, and practical accessibility considerations for mathematical content on the web.</span><br><a title="See more about A11y Considerations in Math on the Web on a new tab" href="https://www.htmhell.dev/adventcalendar/2025/12/" target="_blank" rel="nofollow noopener" class="text-blue-600 underline dark:text-sky-400">See more</a></li><li class="mb-3"><strong>WAI-ARIA 1.3: What Developers Need to Know for Better Accessibility</strong><br><span class="text-sm italic">2025-07-01</span><span><br>Discover new roles and properties that enhance web accessibility for diverse user needs.</span><br><a title="See more about WAI-ARIA 1.3: What Developers Need to Know for Better Accessibility on a new tab" href="/blog/accessibility-wai-aria" target="_blank" rel="nofollow noopener" class="text-blue-600 underline dark:text-sky-400">See more</a></li><li class="mb-3"><strong>Why <s>, <del>, and <ins> Don't Speak: Accessible Alternatives for Text-Level Semantics</strong><br><span class="text-sm italic">2025-04-18</span><span><br>Learn why HTML elements like <s>, <del>, and <ins> aren't reliably announced by screen readers, and how to enhance their accessibility using CSS pseudo-elements and visually hidden content.</span><br><a title="See more about Why <s>, <del>, and <ins> Don't Speak: Accessible Alternatives for Text-Level Semantics on a new tab" href="/blog/accessible-del-ins-s-screen-readers" target="_blank" rel="nofollow noopener" class="text-blue-600 underline dark:text-sky-400">See more</a></li><li class="mb-3"><strong>How to Build Accessible Forms with HTML, CSS and JavaScript</strong><br><span class="text-sm italic">2025-04-12</span><span><br>Learn how to build accessible, user-friendly forms with HTML, CSS, and JavaScript. Step-by-step guide aligned with WCAG 2.2 accessibility standards.</span><br><a title="See more about How to Build Accessible Forms with HTML, CSS and JavaScript on a new tab" href="/blog/accessible-html-forms-guide" target="_blank" rel="nofollow noopener" class="text-blue-600 underline dark:text-sky-400">See more</a></li><li class="mb-3"><strong>Math Accessibility on the Web: ARIA math Role & Best Practices</strong><br><span class="text-sm italic">2025-02-02</span><span><br>Discover how to improve math accessibility on the web using the ARIA math role. Learn best practices, see real-world examples, and boost inclusivity.</span><br><a title="See more about Math Accessibility on the Web: ARIA math Role & Best Practices on a new tab" href="/blog/math-accessibility-aria-mathml-best-practices" target="_blank" rel="nofollow noopener" class="text-blue-600 underline dark:text-sky-400">See more</a></li></ul></div></section><section class="border-b border-b-black"><h2 class="mb-4 text-2xl font-bold">2024</h2><div class="mb-6"><h3 class="mb-2 text-xl font-semibold capitalize">articles</h3><ul class="ml-6 list-disc"><li class="mb-3"><strong>Accessible Forms (Part 1): Field Descriptions</strong><br><span class="text-sm italic">2025-09-03</span><span><br>How to really master the art of accessible forms</span><br><a title="See more about Accessible Forms (Part 1): Field Descriptions on a new tab" href="/blog/accessible-forms-field-description" target="_blank" rel="nofollow noopener" class="text-blue-600 underline dark:text-sky-400">See more</a></li><li class="mb-3"><strong>Creating Accessible Forms in React</strong><br><span class="text-sm italic">2024-09-18</span><span><br>Step-by-step guide on building accessible forms using semantic HTML and ARIA.</sp
161an><br><a title="See more about Creating Accessible Forms in React on a new tab" href="/downloads/a11y-form-guide-2024.pdf" target="_blank" rel="nofollow noopener" class="text-blue-600 underline dark:text-sky-400">See more</a></li></ul></div></section><section class="mt-12"><h2 class="mb-4 text-2xl font-bold">Ongoing Projects</h2><ul class="ml-6 list-disc"><li class="mb-3"><strong>Accesibilidad Web</strong> (platform)<br><span class="text-sm italic">2024 - ongoing</span><br>Finalist in the Digital Accessibility Awards. The largest portal about digital accessibility in Spanish.<br><a href="https://www.accesibilidad-web.info" target="_blank" rel="noopener" class="text-blue-600 underline">View project</a></li><li class="mb-3"><strong>a11y-debug-stylesheet</strong> (library)<br><span class="text-sm italic">2025 - ongoing</span><br>Library for detecting accessibility issues using only CSS.<br><a href="https://github.com/manuelsanchez2/a11y-debug-stylesheet" target="_blank" rel="noopener" class="text-blue-600 underline">View project</a></li></ul></section></div></div></div></section></main><footer tabindex="-1" id="footer" aria-label="Section of footer starting with contact and other social platform links" class="bg-primary-bg pt-12 sm:pt-16">
161<script>(()=>{var a=(s,i,o)=>{let r=async()=>{await(await s())()},t=typeof i.value=="object"?i.value:void 0,c={rootMargin:t==null?void 0:t.rootMargin},n=new IntersectionObserver(e=>{for(let l of e)if(l.isIntersecting){n.disconnect(),r();break}},c);for(let e of o.children)n.observe(e)};(self.Astro||(self.Astro={})).visible=a;window.dispatchEvent(new Event("astro:visible"));})();</script>
161<astro-island uid="1axYWE" data-preact-island-id="1" component-url="/_astro/Newsletter.C4jXl0xI.js?dpl=6aa91bb25e381f000817a421" component-export="Newsletter" renderer-url="/_astro/client.Ca1pjHMH.js?dpl=6aa91bb25e381f000817a421" props="{}" ssr client="visible" opts="{"name":"Newsletter","value":true}" await-children><section aria-label="Newsletter section" class="relative"><div class="absolute inset-x-0 bottom-0 h-1/2 bg-slate-900 dark:bg-black"></div><div class="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"><div class="relative overflow-hidden rounded-2xl bg-sky-700 px-5 py-12 sm:px-16 lg:py-14 dark:border dark:border-white dark:bg-black"><div class="relative flex w-full flex-col items-center lg:flex-row"><div class="max-w-2xl text-center lg:pr-4 lg:text-left"><h2 class="font-display text-4xl font-semibold text-white sm:text-5xl">Subscribe to my newsletter</h2><p class="mx-auto mt-4 max-w-lg text-lg text-sky-50 lg:mx-0 lg:mt-6">Join tons of devs and designers that already receive my content. Do not worry, I also hate spam.</p></div><form class="relative mt-10 w-full max-w-lg lg:mt-0"><label for="7c83cae4-bb40-4472-aaaf-c417a5ffe423" class="sr-only">Write here your email</label><input class="h-14 w-full rounded-full border-0 bg-white/10 py-3.5 pr-32 pl-5 text-sm leading-5 text-sky-50 placeholder-sky-100/90 ring-1 ring-white/25 backdrop-blur duration-200 ease-in-out outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-4 focus:outline-none sm:pl-6" required type="email" autocomplete="email" name="email" id="7c83cae4-bb40-4472-aaaf-c417a5ffe423" placeholder="[email protected]"/><button type="submit" class="sm:text-md absolute top-1.5 right-1.5 inline-flex h-11 cursor-pointer items-center rounded-full bg-sky-900 px-5 py-3 text-sm font-semibold text-sky-50 transition duration-200 ease-in-out hover:bg-sky-800 focus:outline-2 focus:outline-offset-4 disabled:cursor-not-allowed disabled:bg-sky-700/50 disabled:text-sky-300 sm:px-7 dark:bg-white/80 dark:text-black dark:hover:bg-white/90">Sign up!</button></form></div></div></div></section><!--astro:end--></astro-island><div class="overflow-hidden bg-slate-900 pt-20 pb-8 sm:pt-24 sm:pb-12 lg:pt-32 dark:bg-black"><div class="mx-auto max-w-screen-xl px-5 sm:px-6 lg:px-8"><div class="mx-auto grid max-w-xl items-center gap-5 lg:mx-0 lg:max-w-none lg:grid-cols-12 lg:gap-12 xl:gap-20"><div class="lg:col-span-7"><h2 class="font-display text-center text-4xl font-semibold text-white sm:text-5xl lg:max-w-xl lg:text-left">Interested in working together?</h2><div class="hidden lg:block"><a href="mailto:[email protected]" class="inline-flex items-center rounded-full gap-2.5 justify-center px-7 py-3 text-md font-semibold leading-none outline-offset-2 transition-all duration-200 ease-in-out active:transition-none bg-white hover:bg-sky-50 text-slate-700 mt-12">Contact me</a></div></div><div class="flex flex-col items-center lg:col-span-5 lg:items-start"><p class="text-center text-lg text-slate-50 lg:max-w-sm lg:text-left">You can also find me over here.</p><a href="mailto:[email protected]" class="inline-flex items-center rounded-full gap-2.5 justify-center px-7 py-3 text-md font-semibold leading-none outline-offset-2 transition-all duration-200 ease-in-out active:transition-none bg-white hover:bg-sky-50 text-slate-700 mt-10 lg:hidden">Contact me</a><div class="mt-16 grid w-full max-w-sm grid-cols-2 gap-3.5 sm:max-w-none sm:grid-cols-3 lg:mt-8 lg:gap-2.5 xl:gap-3.5"><a class="flex items-center justify-center gap-2.5 rounded-full border border-slate-600/90 py-2.5 text-sm text-slate-50 duration-200 ease-in-out hover:bg-slate-800 hover:text-white lg:gap-2 xl:gap-2.5 dark:border-white dark:hover:bg-white dark:hover:text-black" href="https://www.linkedin.com/in/manusanchez2/" rel="noopener noreferrer" target="_blank">LinkedIn</a><a class="flex items-center justify-center gap-2.5 rounded-full border border-slate-600/90 py-2.5 text-sm text-slate-50 duration-200 ease-in-out hover:bg-slate-800 hover:text-white lg:gap-2 xl:gap-2.5 dark:border-white dark:hover:bg-white dark:hover:text-black" href="https://github.com/manuelsanchez2" rel="noopener noreferrer" target="_blank">Github</a><a class="flex items-center justify-center gap-2.5 rounded-full border border-slate-600/90 py-2.5 text-sm text-slate-50 duration-200 ease-in-out hover:bg-slate-800 hover:text-white lg:gap-2 xl:gap-2.5 dark:border-white dark:hover:bg-white dark:hover:text-black" href="https://bsky.app/profile/manuelsanchezdev.com" rel="noopener noreferrer" target="_blank">Bluesky</a></div></div></div><hr class="mt-12 mb-6 h-px w-full border-slate-600/90 sm:mt-16 sm:mb-10"><div class="flex flex-col items-center justify-between md:flex-row"><div class="flex flex-wrap items-center justify-center gap-6"><a href="/" class="text-base font-medium text-slate-300 duration-200 ease-in-out hover:text-white">Home</a><a href="/blog" class="text-base font-medium text-slate-300 duration-200 ease-in-out hover:text-white">Blog</a><a href="/products" class="text-base font-medium text-slate-300 duration-200 ease-in-out hover:text-white">Products</a><a href="/accessibility-statement" class="text-base font-medium text-slate-300 duration-200 ease-in-out hover:text-white">Accessibility</a><a href="/sitemap" class="text-base font-medium text-slate-300 duration-200 ease-in-out hover:text-white">Sitemap</a>
161<a href="/rss.xml" class="text-base font-medium text-slate-300 duration-200 ease-in-out hover:text-white">RSS</a><div class="text-slate-300 duration-200 ease-in-out hover:text-white"><button type="button" data-cc="show-preferencesModal">Show preferences</button>
161<script type="module" src="/_astro/CookieConsent.astro_astro_type_script_index_0_lang.DnrSHB7h.js?dpl=6aa91bb25e381f000817a421"></script>
161</div></div><p class="mt-8 text-center text-base text-slate-400/90 md:mt-0">© 2026 Manuel Sánchez. All rights reserved.</p></div><div class="mt-8 flex justify-center"><a href="#main-content" class="text-sm text-slate-400 underline underline-offset-4 transition duration-200 hover:text-white">â Back to top</a></div></div></div></footer><dialog id="newsletter-dialog" aria-labelledby="newsletter-title" aria-describedby="newsletter-desc" class="w-[92vw] max-w-md rounded-2xl border border-slate-200 p-0 shadow-2xl backdrop:backdrop-blur" data-delay-ms="20000" data-storage-key="newsletter_dismissed" data-endpoint="/api/newsletter"><!-- Hero placeholder --><div class="relative h-30 w-full rounded-t-2xl bg-blue-400"><button type="button" data-close class="absolute top-3 right-3 inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full bg-white/90 ring-1 ring-slate-300 hover:bg-white focus:outline-none" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg></button><figure class="absolute -bottom-4 left-1/2 flex w-fit -translate-x-1/2 justify-center overflow-hidden rounded-md outline-8 outline-offset-4 outline-white"><img src="/.netlify/images?url=_astro%2Fmanuel-pixelart.Ber5t_Nq.png&w=60&h=60&dpl=6aa91bb25e381f000817a421" alt loading="lazy" decoding="async" width="60" height="60"></figure></div><!-- Body --><div class="px-6 py-6 pb-5"><!-- <p class="mb-2 text-sm text-slate-500" id="newsletter-desc"> 162 <span 163 class="inline-flex items-center gap-2 rounded-full border border-slate-200 px-3 py-1 text-xs" 164 > 165 <span class="i" aria-hidden="true">ð¬</span> 111 subscribed yesterday 166 </span> 167 </p> --><h2 id="newsletter-title" class="text-2xl font-semibold tracking-tight">Stay in the loop!</h2><p class="mt-2 text-slate-600">Get to know some good resources. Once per month.</p><small>Frontend & Game Development, tools that make my life easier, newest blog posts and resources, codepens or some snippets. All for free!</small><form id="newsletter-form" class="mt-5 grid gap-3" novalidate><label class="sr-only" for="newsletter-email">Email address</label><input id="newsletter-email" name="email" type="email" required autocomplete="email" placeholder="Email address" class="w-full rounded-full border border-slate-300 px-4 py-3 outline-none focus:ring-2 focus:ring-black/70"><button id="newsletter-submit" type="submit" class="w-full rounded-full bg-black px-5 py-3 font-medium text-white hover:bg-black/90 disabled:cursor-not-allowed disabled:opacity-60">Subscribe for free</button><p id="newsletter-hint" class="text-center text-xs text-slate-500">No spam, just cool stuff. Promised. Unsubscribe anytime.</p><p id="newsletter-feedback" class="text-center text-sm"></p></form></div></dialog><!-- Mounting anchor --><!-- <div id="newsletter-anchor"></div> -->
167<script type="module"> 168 /** 169 * Minimal controller for the newsletter modal. 170 * Reads config from data-* attributes on the dialog. 171 * @typedef {Object} Ctx 172 * @property {HTMLDialogElement} dialog 173 * @property {HTMLFormElement} form 174 * @property {HTMLInputElement} email 175 * @property {HTMLButtonElement} submit 176 * @property {HTMLElement} feedback 177 */ 178 179 /** Safely set a boolean flag in localStorage. */ 180 const setFlag = (key, value = true) => { 181 try { 182 localStorage.setItem(key, value ? '1' : '0') 183 } catch {} 184 } 185 /** Safely read a boolean flag from localStorage. */ 186 const getFlag = (key) => { 187 try { 188 return localStorage.getItem(key) === '1' 189 } catch { 190 return false 191 } 192 } 193 194 /** @returns {Ctx | null} */ 195 const bind = () => { 196 const dialog = /** @type {HTMLDialogElement} */ ( 197 document.getElementById('newsletter-dialog') 198 ) 199 const form = /** @type {HTMLFormElement} */ ( 200 document.getElementById('newsletter-form') 201 ) 202 const email = /** @type {HTMLInputElement} */ ( 203 document.getElementById('newsletter-email') 204 ) 205 const submit = /** @type {HTMLButtonElement} */ ( 206 document.getElementById('newsletter-submit') 207 ) 208 const feedback = /** @type {HTMLElement} */ ( 209 document.getElementById('newsletter-feedback') 210 ) 211 if (!dialog || !form || !email || !submit || !feedback) return null 212 return { dialog, form, email, submit, feedback } 213 } 214 215 let closedBySubscribe = false 216 217 const ctx = bind() 218 if (ctx) { 219 // Read config from data attributes 220 const delayAttr = ctx.dialog.dataset.delayMs 221 const storageKeyAttr = 222 ctx.dialog.dataset.storageKey || 'newsletter_dismissed' 223 const endpointAttr = ctx.dialog.dataset.endpoint || '/api/subscribe' 224 225 const DELAY = Number(delayAttr ?? 20000) 226 const STORAGE_KEY = storageKeyAttr 227 const ENDPOINT = endpointAttr 228 229 if (!getFlag(STORAGE_KEY) && !getFlag('newsletter_subscribed')) { 230 const timer = setTimeout(() => { 231 ctx.dialog.showModal() 232 document.documentElement.setAttribute('data-newsletter-open', 'true') 233 // const anchor = document.getElementById('newsletter-anchor') 234 // if (anchor) anchor.appendChild(ctx.dialog) 235 queueMicrotask(() => ctx.email.focus()) 236 }, DELAY) 237 238 ctx.dialog.addEventListener('close', () => { 239 document.documentElement.removeAttribute('data-newsletter-open') 240 if (!closedBySubscribe) setFlag(STORAGE_KEY, true) 241 }) 242 243 document.querySelector('[data-close]')?.addEventListener('click', () => { 244 ctx.dialog.close('dismiss') 245 }) 246 247 ctx.form.addEventListener('submit', async (e) => { 248 e.preventDefault() 249 const emailValue = ctx.email.value.trim() 250 251 if (!emailValue || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailValue)) { 252 ctx.feedback.textContent = 'Please enter a valid email.' 253 ctx.feedback.className = 'text-center text-sm text-red-600' 254 return 255 } 256 257 ctx.submit.disabled = true 258 ctx.feedback.textContent = 'Subscribingâ¦' 259 ctx.feedback.className = 'text-center text-sm text-slate-500' 260 261 try { 262 const res = await fetch(ENDPOINT, { 263 method: 'POST', 264 headers: { 'Content-Type': 'application/json' }, 265 body: JSON.stringify({ email: emailValue }), 266 }) 267 268 if (res.status === 200 || res.status === 409) { 269 ctx.feedback.textContent = 270 res.status === 200 271 ? 'You are subscribed. Check your inbox!' 272 : 'You are already subscribed. ð' 273 ctx.feedback.className = 'text-center text-sm text-emerald-600' 274 setFlag('newsletter_subscribed', true) 275 setFlag(STORAGE_KEY, true) 276 closedBySubscribe = true 277 // reset the form 278 ctx.form.reset() 279 // setTimeout( 280 // () => 281 // ctx.dialog.close(res.status === 200 ? 'subscribed' : 'already'), 282 // 900 283 // ) 284 return 285 } 286 287 ctx.feedback.textContent = 288 'Something went wrong. Please try again later.' 289 ctx.feedback.className = 'text-center text-sm text-red-600' 290 } catch { 291 ctx.feedback.textContent = 'Network error. Please try again.' 292 ctx.feedback.className = 'text-center text-sm text-red-600' 293 } finally { 294 ctx.submit.disabled = false 295 } 296 }) 297 298 window.addEventListener('beforeunload', () => clearTimeout(timer)) 299 } 300 } 301</script>
301<div id="footer-bar" role="region" aria-label="Site announcement" class="fixed inset-x-0 bottom-0 z-40 hidden translate-y-0 border-t border-slate-200 bg-white/95 backdrop-blur transition-transform duration-300 ease-out dark:border-slate-700 dark:bg-black/95" data-storage-key="footerbar_dismissed_at" data-animate="true" data-show-delay-ms="2000"><div class="mx-auto flex max-w-screen-xl items-center justify-between gap-4 px-5 py-3 sm:px-6 lg:px-8"><p class="flex min-w-0 items-center gap-2 text-sm text-slate-700 dark:text-slate-300"><span aria-hidden="true">ð®</span><span class="truncate">My first game will release soon.<a href="https://barkhallastudios.com" target="_blank" rel="noopener noreferrer" class="no-external-icon ml-2 font-medium text-slate-900 underline underline-offset-2 dark:text-white" data-track-event data-track-event-type="click" data-track-event-id="footerbar-cta" data-track-event-project="portfolio-astro" data-track-event-properties="{"href":"https://barkhallastudios.com","linkText":"Take a look"}">Take a look</a></span></p><button type="button" data-footer-bar-close class="inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-slate-500 hover:bg-slate-100 hover:text-slate-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white" aria-label="Close"><svg viewBox="0 0 24 24" class="h-4 w-4" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg></button></div></div>
301<script> 302 ;(function () { 303 const ONE_MONTH_MS = 30 * 24 * 60 * 60 * 1000 304 305 const isDismissed = (key) => { 306 try { 307 const stored = localStorage.getItem(key) 308 if (!stored) return false 309 const dismissedAt = Number(stored) 310 if (Number.isNaN(dismissedAt)) return false 311 return Date.now() - dismissedAt < ONE_MONTH_MS 312 } catch { 313 return false 314 } 315 } 316 317 const setDismissed = (key) => { 318 try { 319 localStorage.setItem(key, String(Date.now())) 320 } catch {} 321 } 322 323 const show = (bar, animate) => { 324 if (!animate) { 325 bar.classList.remove('hidden') 326 return 327 } 328 329 // Start off-screen, then animate up on the next frame so the 330 // transition actually plays instead of jumping straight to place. 331 bar.classList.add('translate-y-full') 332 bar.classList.remove('translate-y-0') 333 bar.classList.remove('hidden') 334 void bar.offsetHeight // force reflow to register the start position 335 requestAnimationFrame(() => { 336 bar.classList.remove('translate-y-full') 337 bar.classList.add('translate-y-0') 338 }) 339 } 340 341 const init = () => { 342 const bar = document.getElementById('footer-bar') 343 if (!bar) return 344 345 const storageKey = bar.dataset.storageKey || 'footerbar_dismissed_at' 346 347 if (isDismissed(storageKey)) { 348 bar.remove() 349 return 350 } 351 352 bar 353 .querySelector('[data-footer-bar-close]') 354 ?.addEventListener('click', () => { 355 setDismissed(storageKey) 356 bar.remove() 357 }) 358 359 const showDelayMs = Number(bar.dataset.showDelayMs ?? 2000) 360 const animate = bar.dataset.animate === 'true' 361 362 let triggered = false 363 const trigger = () => { 364 if (triggered) return 365 triggered = true 366 setTimeout(() => show(bar, animate), showDelayMs) 367 } 368 369 window.addEventListener('scroll', trigger, { once: true, passive: true }) 370 } 371 372 document.addEventListener('astro:page-load', init) 373 })() 374</script>
374</body>
374<script type="application/ld+json">{"@context":"https://schema.org","@type":"Person","@id":"https://manuelsanchezdev.com/#person","name":"Manuel Sánchez","alternateName":"Manuel Sanchez","url":"https://manuelsanchezdev.com","sameAs":["https://github.com/manuelsanchez2","https://www.linkedin.com/in/manusanchez2/","https://www.youtube.com/channel/UCX3IE_OjG20p_AwbX06YAEg"]}</script>
374</html>
374<script type="module">document.addEventListener(`astro:page-load`,()=>{let e=(e,t)=>`<svg xmlns="http://www.w3.org/2000/svg" width="${e}" height="${e}" viewBox="0 0 16 16" aria-hidden="true" focusable="false"> 375 <path fill="${t}" fill-rule="evenodd" clip-rule="evenodd" 376 d="M5.75 2H5v1.5h.75h5.69L2.22 12.72l-.53.53 1.06 1.06.53-.53 9.22-9.22V10.25V11h1.5v-.75V3c0-.55-.45-1-1-1H5.75Z"/> 377 </svg>`;document.querySelectorAll(`a[target="_blank"]:not(:has(img)):not(.no-external-icon)`).forEach(t=>{let n=document.createElement(`span`);n.innerHTML=e(`16`,`currentColor`),n.classList.add(`ml-1`,`inline-block`,`align-middle`),t.appendChild(n),t.classList.add(`inline-flex`,`items-center`,`gap-1`)})});</script>
377<script type="module" astro:page-load>const publicAnalyticsEnabled = ""; 378const isDev = false; 379 380 let lastTrackedPath = null 381 const analyticsEndpoint = 'https://analytics-manuel.netlify.app/api/events' 382 const localHostnames = new Set(['localhost', '127.0.0.1', '0.0.0.0']) 383 const analyticsFlag = publicAnalyticsEnabled?.trim().toLowerCase() 384 385 function isAnalyticsEnabled() { 386 if (analyticsFlag === 'true') return true 387 if (analyticsFlag === 'false') return false 388 if (isDev) return false 389 if (localHostnames.has(window.location.hostname)) return false 390 391 return true 392 } 393 394 function getDeviceType() { 395 const width = window.innerWidth 396 if (width <= 768) return 'mobile' 397 if (width <= 1024) return 'tablet' 398 return 'desktop' 399 } 400 401 async function trackPageView() { 402 if (!isAnalyticsEnabled()) return 403 404 const path = window.location.pathname + window.location.search 405 406 try { 407 await fetch(analyticsEndpoint, { 408 method: 'POST', 409 headers: { 'Content-Type': 'application/json' }, 410 body: JSON.stringify({ 411 eventType: 'view', 412 eventId: path, 413 projectSlug: 'portfolio-astro', 414 deviceType: getDeviceType(), 415 properties: { path }, 416 }), 417 }) 418 } catch (err) { 419 console.error('Error sending pageview:', err) 420 } 421 } 422 423 async function handlePageView() { 424 const path = window.location.pathname + window.location.search 425 426 // Prevent double calls for the same URL 427 if (path === lastTrackedPath) return 428 lastTrackedPath = path 429 430 await trackPageView() 431 } 432 433 // Track initial load 434 handlePageView() 435 436 // Track navigation swaps 437 438 // document.addEventListener('astro:after-swap', handlePageView) 439 440 async function trackEvent({ 441 eventType = 'click', 442 eventId = '', 443 projectSlug = 'portfolio-astro', 444 properties = {}, 445 }) { 446 if (!isAnalyticsEnabled()) return 447 448 try { 449 const response = await fetch(analyticsEndpoint, { 450 method: 'POST', 451 headers: { 'Content-Type': 'application/json' }, 452 body: JSON.stringify({ 453 eventType, 454 eventId, 455 projectSlug, 456 deviceType: getDeviceType(), 457 properties, 458 }), 459 }) 460 461 if (!response.ok) { 462 console.error('Failed to track event') 463 } 464 } catch (error) { 465 console.error('Error sending event:', error) 466 } 467 } 468 469 function attachClickTrackingListeners() { 470 document.querySelectorAll('[data-track-event]').forEach((el) => { 471 // Avoid double attaching 472 if (el.dataset.tracked) return 473 el.dataset.tracked = 'true' 474 475 el.addEventListener('click', () => { 476 const type = el.getAttribute('data-track-event-type') || 'click' 477 const id = el.getAttribute('data-track-event-id') 478 const project = el.getAttribute('data-track-event-project') 479 const properties = el.getAttribute('data-track-event-properties') 480 481 if (type && id) { 482 const props = properties ? JSON.parse(properties) : {} 483 if (!props.path) { 484 props.path = window.location.pathname + window.location.search 485 } 486 487 trackEvent({ 488 eventType: type, 489 eventId: id, 490 projectSlug: project || 'portfolio-astro', 491 properties: props, 492 }) 493 } 494 }) 495 }) 496 } 497 498 // Run immediately on page-load 499 attachClickTrackingListeners() 500 501 // Re-run whenever Astro swaps the page during navigation 502 document.addEventListener('astro:after-swap', attachClickTrackingListeners) 503</script>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.