- Remove disruptive alert() on error pages, rely on role="alert" banner
- Add skip navigation link for keyboard users (WCAG 2.4.1)
- Fix heading hierarchy on error page (sr-only status text in h1)
- Add "opens in a new tab" screen reader hint on external links
- Wrap language selector in form with noscript submit button (WCAG 3.2.2)
- Add aria-label to copy-to-clipboard buttons and footer landmarks
- Use h2 for alt-IP section to maintain single h1 per page
- Add persistent underline on links for non-color identification (WCAG 1.4.1)
- Fix footer attribution contrast ratio to ~5.5:1 (WCAG 1.4.3)
- Add aria-busy/role=status loading announcements for skeleton state
- Add translations for new accessibility strings across all 14 locales
- Update integration tests to assert role="alert" instead of alert()
Rewrite script.js from ES5 (IIFEs, var, .then() chains) to modern ES2022+:
const/let, arrow functions, template literals, async/await, optional
chaining, for...of, .includes(), AbortSignal.timeout(), and DOM APIs
for dynamic HTML construction. Fix unhandled clipboard rejection, abort
timeout leak, and global scope pollution. Update script tag to
type="module" and fix CSS: top -> inset-block-start, flex-start -> start,
and indentation in media query block.
The JS used fragile numeric indices to look up localized labels from the
server-rendered table. When optional rows (ASN, Org, Country, City) were
missing for the primary IP, the indices shifted, causing 'Proxy' to appear
as the City label. Add data-label attributes to each <tr> in the template
and key the label map by name instead of position.
Show a shimmer skeleton placeholder while fetching the local IPv4
address via the IPv4-only domain. When the primary IP is already IPv4,
no loading state is displayed and the alt section fades in directly.
The detectAltIp() function was unconditionally fetching the viewer's own
IPv6/IPv4 address and displaying it alongside queried IP results. Pass
the is_query flag to the client via a data attribute and guard the
alt-IP detection so it only runs when viewing your own IP.