{"id":"l-css-3-5","courseId":"css-mastery","lang":"en","kind":"lesson","summary":"Build focus indicators that keyboard users can always see, with :focus-visible, outlines that survive forced-colors mode, and scroll padding that keeps focus out from under a sticky header.","html":"<p>Open Waypoint’s schedule, put your mouse away, and press Tab. On the first version you’d see nothing. The reset contained <code dir=\"ltr\">:focus { outline: none; }</code>, added because a designer disliked the ring that appeared when clicking buttons. Keyboard users, switch users and many screen-magnifier users navigate by focus, and they were navigating blind.</p>\n<p>Removing focus styles isn’t a style choice; it’s a broken page for a large group of people. The good news is that modern CSS makes good focus styles easy to get right.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">:focus-visible: the ring when it helps</h2>\n<p>The designer’s complaint was real: a ring after every mouse click looks like a glitch. <code dir=\"ltr\">:focus-visible</code> solves it. It matches a focused element only when the browser judges that an indicator is useful:</p>\n<ul>\n<li>Focus arrived by keyboard: yes.</li>\n<li>A mouse click focused a button or link: no.</li>\n<li>A text field received focus, by any means: yes, because you need to see where you’re typing.</li>\n</ul>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">focus.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:root</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: light-dark(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#6d28d9</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#c4b5fd</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:focus-visible</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> solid</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline-offset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>That one rule replaces the reset’s damage. It applies to every focusable element, keyboard users get a clear ring, and mouse users don’t see it on buttons. <code dir=\"ltr\">:focus-visible</code> is Baseline widely available.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Why outline beats box-shadow</h2>\n<p>Many design systems draw focus rings with <code dir=\"ltr\">box-shadow</code> because shadows used to follow rounded corners and outlines didn’t. Outlines have followed <code dir=\"ltr\">border-radius</code> in all current browsers since 2023, and they have three advantages:</p>\n<ol>\n<li><strong>No layout shift.</strong> Outlines don’t take up space, so focusing a button never nudges its neighbours.</li>\n<li><strong><code dir=\"ltr\">outline-offset</code></strong> puts a gap between the element and the ring, which keeps the ring visible against the element’s own background.</li>\n<li><strong>Forced-colors mode.</strong> Windows high-contrast themes remove <code dir=\"ltr\">box-shadow</code> entirely but keep outlines, recoloured to a system color.</li>\n</ol>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Killing the ring and drawing it with a shadow</span></p>\n<p><code dir=\"ltr\">outline: none; box-shadow: 0 0 0 3px …</code> looks identical on your screen and vanishes in forced-colors mode. If you must use a shadow for a special effect, keep <code dir=\"ltr\">outline: 2px solid transparent</code> underneath: a transparent outline is invisible normally, and forced colors paints it in a visible system color.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Contrast in both themes</h2>\n<p>A focus indicator needs at least 3:1 contrast against what’s around it (WCAG’s non-text contrast). A violet that works on white can disappear on a dark slate surface. Defining <code dir=\"ltr\">--focus</code> with <code dir=\"ltr\">light-dark()</code>, as above, gives each theme its own ring color from one token: a deep violet on light surfaces, a pale lavender on dark ones.</p>\n<p>For components that sit on unpredictable backgrounds (the hero image, a colored track badge), a two-tone ring is safest: an outline in the focus color plus a contrasting halo just inside it:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.hero</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> :focus-visible</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> solid</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline-offset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  box-shadow</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--surface</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);  </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* halo between element and ring */</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Here the shadow is decoration on top of a real outline, so forced-colors users still get the outline.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Focus a whole card</h2>\n<p>Waypoint’s session cards are clickable: the title is a link, stretched to cover the card with a pseudo-element. The ring then appears around the title text only, which looks odd. Move it to the card with <code dir=\"ltr\">:has()</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card:has</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#116329;--shiki-dark:#7EE787\">a</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:focus-visible</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> solid</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline-offset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> a</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:focus-visible</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">none</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;   </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* the card shows the ring instead */</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Removing the link’s own ring is fine here, because the card’s ring replaces it whenever the link has visible focus. That’s the only kind of <code dir=\"ltr\">outline: none</code> you should ship: one that’s covered by another indicator.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Don’t let the header hide focus</h2>\n<p>WCAG 2.2 added <em>Focus Not Obscured</em>: the focused element can’t be entirely hidden by content the author put on top, like a sticky header or a cookie banner. Waypoint’s header is sticky and 64px tall, and when a user tabs backwards, the browser scrolls the focused card to the top of the viewport, right under the header.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">html</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  scroll-padding-top</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">64</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Scroll padding tells the browser that part of the scroll container’s viewport is covered. Focus scrolling, anchor links and <code dir=\"ltr\">scrollIntoView()</code> all respect it. If the header’s height changes, store it in a custom property and use it in both places.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Test with the keyboard for one minute</span></p>\n<p>Unplug the habit, not the mouse: press Tab through your page from top to bottom, then Shift+Tab back. Can you always see where you are? Does anything get hidden? Do it in both themes. It takes a minute and catches most focus bugs.</p>\n</aside>\n<h2 id=\"sec-6\" tabindex=\"-1\">Your turn</h2>\n<p>The exercise page has the bad reset. Replace it with a <code dir=\"ltr\">:focus-visible</code> ring, move the ring to the card when its link is focused, and add scroll padding for the 64px header. That finishes the responsive section; next, you’ll add motion, starting with transitions, keyframes and respect for reduced-motion settings.</p>\n","toc":[{"id":"sec-1","text":":focus-visible: the ring when it helps","level":2},{"id":"sec-2","text":"Why outline beats box-shadow","level":2},{"id":"sec-3","text":"Contrast in both themes","level":2},{"id":"sec-4","text":"Focus a whole card","level":2},{"id":"sec-5","text":"Don’t let the header hide focus","level":2},{"id":"sec-6","text":"Your turn","level":2}],"takeaways":["<code dir=\"ltr\">:focus-visible</code> matches when the browser decides a focus indicator is useful, typically keyboard navigation, so you can show a strong ring without showing it on every mouse click.","Use <code dir=\"ltr\">outline</code> with <code dir=\"ltr\">outline-offset</code> for focus rings; it doesn’t change layout, follows <code dir=\"ltr\">border-radius</code>, and stays visible in Windows forced-colors mode where <code dir=\"ltr\">box-shadow</code> disappears.","Define the focus color with <code dir=\"ltr\">light-dark()</code> so each theme gets its own ring color, then check it reaches at least 3:1 contrast against the surface in both.","<code dir=\"ltr\">scroll-padding-top</code> on the scroll container stops a sticky header from covering the element that just received focus."],"further":[{"title":":focus-visible (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:focus-visible"},{"title":"Understanding Success Criterion 2.4.11: Focus Not Obscured (Minimum) (W3C)","url":"https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html"},{"title":"Focus (web.dev Learn CSS)","url":"https://web.dev/learn/css/focus"}],"quiz":[{"q":"<p>Why is <code dir=\"ltr\">button:focus { outline: none; box-shadow: 0 0 0 3px violet; }</code> a risky focus style?</p>","options":[{"text":"Box shadows are slower to paint than outlines.","why":"The paint cost difference is negligible; the problem is visibility for some users."},{"text":"Windows forced-colors mode removes box shadows, so keyboard users in high-contrast mode see no focus at all.","why":"Correct. Forced colors strips shadows but keeps outlines (recoloured to a system color), so the outline is the robust choice."},{"text":"<code dir=\"ltr\">box-shadow</code> doesn’t follow <code dir=\"ltr\">border-radius</code>.","why":"Box shadows do follow the border radius; that was never the issue."}],"answer":1},{"q":"<p>You want a focus ring on keyboard focus but not when a mouse user clicks a button. Which selector?</p>","options":[{"text":"<code dir=\"ltr\">:focus</code>","why":"<code dir=\"ltr\">:focus</code> matches every focus, including after a mouse click, so the ring appears on click too."},{"text":"<code dir=\"ltr\">:focus-within</code>","why":"<code dir=\"ltr\">:focus-within</code> matches an ancestor of the focused element; it doesn’t distinguish keyboard from mouse."},{"text":"<code dir=\"ltr\">:active</code>","why":"<code dir=\"ltr\">:active</code> matches while a pointer is pressed down, which is the opposite of what you want."},{"text":"<code dir=\"ltr\">:focus-visible</code>","why":"Correct. The browser applies its heuristics (keyboard yes, mouse click on a button no, text fields always) and matches only when an indicator helps."}],"answer":3},{"q":"<p>Waypoint’s header is <code dir=\"ltr\">position: sticky</code> and 64px tall. When users tab backwards through the schedule, the focused card scrolls into view hidden under the header. What fixes it?</p>","options":[{"text":"<code dir=\"ltr\">html { scroll-padding-top: 64px; }</code>","why":"Correct. Scroll padding tells the browser the top 64px of the viewport is obscured, so focus and anchor scrolling stop below it."},{"text":"<code dir=\"ltr\">.session-card { margin-top: 64px; }</code>","why":"That adds a 64px gap above every card on the page, all the time, just to work around scrolling."},{"text":"<code dir=\"ltr\">header { z-index: -1; }</code>","why":"The header would then be painted behind the content, which breaks the sticky header entirely."}],"answer":0}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<header class=\"site-header\">Waypoint Conf 2026 <button type=\"button\" id=\"tickets\">Get tickets</button></header>\n<main>\n  <article class=\"session-card\">\n    <h3><a href=\"#s1\" id=\"card-link\">Focus Styles That Work</a></h3>\n    <p>Rings, offsets and forced colors.</p>\n  </article>\n</main>","css":"/* Reset (from the prototype) */\n:focus { outline: none; }\n\n:root {\n  --focus: light-dark(#6d28d9, #c4b5fd);\n}\n.site-header {\n  position: sticky;\n  top: 0;\n  height: 64px;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  background: white;\n}\n.session-card {\n  padding: 12px;\n  border: 1px solid #e5e7eb;\n  border-radius: 12px;\n}\n"},"solution":{"html":"<header class=\"site-header\">Waypoint Conf 2026 <button type=\"button\" id=\"tickets\">Get tickets</button></header>\n<main>\n  <article class=\"session-card\">\n    <h3><a href=\"#s1\" id=\"card-link\">Focus Styles That Work</a></h3>\n    <p>Rings, offsets and forced colors.</p>\n  </article>\n</main>","css":":root {\n  --focus: light-dark(#6d28d9, #c4b5fd);\n}\n.site-header {\n  position: sticky;\n  top: 0;\n  height: 64px;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  background: white;\n}\n.session-card {\n  padding: 12px;\n  border: 1px solid #e5e7eb;\n  border-radius: 12px;\n}\n\n:focus-visible {\n  outline: 2px solid var(--focus);\n  outline-offset: 2px;\n}\n\n.session-card:has(a:focus-visible) {\n  outline: 2px solid var(--focus);\n  outline-offset: 4px;\n}\n.session-card a:focus-visible {\n  outline: none;\n}\n\nhtml {\n  scroll-padding-top: 64px;\n}\n"},"checks":[{"id":"button-ring","test":"const all = []; const walk = (rules) => { for (const r of rules) { all.push(r); if (r.cssRules) walk(r.cssRules); } }; for (const s of document.styleSheets) walk(s.cssRules); const rules = all.filter((r) => r.selectorText); const b = $('#tickets'); b.focus(); await tick(16); let ok; if (b.matches(':focus-visible')) { ok = style(b, 'outline-style') === 'solid' && parseFloat(style(b, 'outline-width')) >= 2 && style(b, 'outline-color') === 'rgb(109, 40, 217)'; } else { ok = rules.some((r) => r.selectorText.includes(':focus-visible') && !r.selectorText.includes(':has(') && r.style.outlineStyle === 'solid'); } b.blur(); return ok;"},{"id":"ring-offset","test":"const all = []; const walk = (rules) => { for (const r of rules) { all.push(r); if (r.cssRules) walk(r.cssRules); } }; for (const s of document.styleSheets) walk(s.cssRules); const rules = all.filter((r) => r.selectorText); const b = $('#tickets'); b.focus(); await tick(16); let ok; if (b.matches(':focus-visible')) { ok = parseFloat(style(b, 'outline-offset')) >= 2; } else { ok = rules.some((r) => r.selectorText.includes(':focus-visible') && parseFloat(r.style.outlineOffset) >= 2); } b.blur(); return ok;"},{"id":"no-ring-unfocused","test":"$('#tickets').blur(); await tick(16); return style('#tickets', 'outline-style') === 'none';"},{"id":"card-ring","test":"const all = []; const walk = (rules) => { for (const r of rules) { all.push(r); if (r.cssRules) walk(r.cssRules); } }; for (const s of document.styleSheets) walk(s.cssRules); const rules = all.filter((r) => r.selectorText); const a = $('#card-link'); a.focus(); await tick(16); let ok; if (a.matches(':focus-visible')) { ok = style('.session-card', 'outline-style') === 'solid' && style('.session-card', 'outline-color') === 'rgb(109, 40, 217)'; } else { ok = rules.some((r) => r.selectorText.includes('.session-card:has(') && r.selectorText.includes(':focus-visible') && r.style.outlineStyle === 'solid'); } a.blur(); return ok;"},{"id":"scroll-padding","test":"return parseFloat(style(document.documentElement, 'scroll-padding-top')) === 64;"}],"title":"Bring back the focus ring","prompt":"<p>The prototype’s reset removes every focus outline. Replace it so that:</p>\n<ol>\n<li>Any element with <strong>visible focus</strong> gets a <code dir=\"ltr\">2px solid</code> outline in <code dir=\"ltr\">var(--focus)</code> with an <code dir=\"ltr\">outline-offset</code> of at least <code dir=\"ltr\">2px</code> (and no ring when it isn’t focused).</li>\n<li>When the card’s link has visible focus, the <strong><code dir=\"ltr\">.session-card</code></strong> shows the same ring (you may hide the link’s own ring in that case).</li>\n<li>Focus scrolling stops below the <strong>64px</strong> sticky header.</li>\n</ol>\n","hints":["Delete the <code dir=\"ltr\">:focus { outline: none; }</code> reset; it’s the cause, not something to work around.","<code dir=\"ltr\">:focus-visible</code> is the selector for the ring; <code dir=\"ltr\">:has(a:focus-visible)</code> lets the card react to its link.","Scroll padding goes on the scroll container, the root: <code dir=\"ltr\">html { scroll-padding-top: 64px; }</code>."],"explanation":"<p><code dir=\"ltr\">:focus-visible</code> shows the outline when the browser judges an indicator helps (keyboard focus, text fields) and skips it after mouse clicks on buttons, which is what the reset was clumsily trying to achieve. An outline doesn’t affect layout and survives forced-colors mode, and <code dir=\"ltr\">outline-offset</code> keeps it clear of the element. <code dir=\"ltr\">.session-card:has(a:focus-visible)</code> moves the ring to the card, so hiding the link’s own outline is safe there. <code dir=\"ltr\">scroll-padding-top: 64px</code> on the root tells the browser the top of the viewport is covered, so focus never lands under the sticky header.</p>\n","checkLabels":{"button-ring":"A focused button gets a 2px solid ring in the focus color","ring-offset":"The ring has an offset of at least 2px","no-ring-unfocused":"No ring when nothing is focused","card-ring":"The card shows the ring when its link is focused","scroll-padding":"Focus scrolling clears the 64px header"}},"words":735,"readMinutes":4}