PageSourceSearch

https://manuelsanchezdev.com/accessibility/

html manuelsanchezdev.com collected 2026-09-25 20:19:44 UTC 54,059 bytes, 503 lines download raw bytes

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-[&quot;&quot;] 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-[&quot;&quot;] 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-[&quot;&quot;] 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="{&quot;name&quot;:&quot;Notifications&quot;,&quot;value&quot;: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 &amp; 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&amp;utm_medium=member_desktop&amp;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 &lt;s&gt;, &lt;del&gt;, and &lt;ins&gt; Don&#39;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 &lt;s&gt;, &lt;del&gt;, and &lt;ins&gt; aren&#39;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 &amp; 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 &amp; 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="{&quot;name&quot;:&quot;Newsletter&quot;,&quot;value&quot;: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&amp;w=60&amp;h=60&amp;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="{&quot;href&quot;:&quot;https://barkhallastudios.com&quot;,&quot;linkText&quot;:&quot;Take a look&quot;}">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.