← Accessible Components results

Accessible Components

Deepseek v4.1 Flash · full

Initial view of Accessible Components

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 60,374 bytes · SHA-256 f8543b5b058a
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accessible Components — a small, keyboard-first library</title>
<meta name="description" content="Five accessible interaction patterns built to the WAI-ARIA Authoring Practices: modal dialog, dropdown menu, tabs, combobox and accordion.">
<style>
/* ==========================================================================
   1. TUNABLE CONSTANTS
   ========================================================================== */
:root{
  /* Palette — restrained, two accents */
  --accent:        #4640d8;
  --accent-strong: #3730b8;
  --accent-soft:   #eeeeff;
  --accent-line:   #c9c6f5;

  --accent2:       #0b7a6e;
  --accent2-soft:  #e8f6f4;
  --accent2-line:  #a9d9d2;

  --danger:        #b3261e;

  --ink:        #14161b;
  --ink-2:      #4b5262;
  --ink-3:      #767e8f;
  --bg:         #ffffff;
  --bg-soft:    #f6f7f9;
  --bg-sunk:    #eef0f4;
  --line:       #e3e6ec;
  --line-2:     #cdd2dc;

  /* Spacing scale */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 72px;

  /* Shape + motion */
  --radius-sm: 6px;
  --radius:    10px;
  --radius-lg: 16px;
  --dur-fast:  120ms;
  --dur:       200ms;
  --dur-slow:  260ms;
  --ease:      cubic-bezier(.2,.75,.3,1);

  --focus:     #1d4ed8;
  --focus-ring: 0 0 0 3px #ffffff, 0 0 0 6px #1d4ed8;

  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  --shadow-1: 0 1px 2px rgba(16,20,32,.06), 0 1px 1px rgba(16,20,32,.04);
  --shadow-2: 0 4px 14px rgba(16,20,32,.09), 0 2px 4px rgba(16,20,32,.05);
  --shadow-3: 0 24px 60px -12px rgba(16,20,32,.32), 0 8px 20px rgba(16,20,32,.12);
  --sidebar-w: 264px;
  --content-max: 68rem;
  color-scheme: light;
}

/* ==========================================================================
   2. BASE
   ========================================================================== */
*,*::before,*::after{ box-sizing: border-box; }
/* The hidden attribute must always win over a display declaration. */
[hidden]{ display: none !important; }
html{ -webkit-text-size-adjust: 100%; }
body{
  margin:0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
h1,h2,h3,h4{ margin:0; line-height:1.2; letter-spacing:-.021em; font-weight:640; }
p{ margin:0; }
ul{ margin:0; padding:0; list-style:none; }
button{ font: inherit; color: inherit; }

/* Universal focus indicator — never remove without a replacement */
:focus-visible{
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
::selection{ background: var(--accent-soft); }

.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); clip-path: inset(50%); white-space:nowrap; border:0;
}

/* ==========================================================================
   3. SKIP LINK
   ========================================================================== */
.skip-link{
  position: fixed;
  top: var(--sp-3);
  left: var(--sp-3);
  z-index: 100;
  transform: translateY(-160%);
  background: var(--accent);
  color:#fff;
  font-weight:600;
  font-size:.9rem;
  padding: 10px 16px;
  border-radius: var(--radius);
  text-decoration:none;
  box-shadow: var(--shadow-2);
  transition: transform var(--dur) var(--ease);
}
.skip-link:focus{ transform: translateY(0); outline-color:#fff; }

/* ==========================================================================
   4. SHELL / LAYOUT
   ========================================================================== */
.app{
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0,1fr);
  align-items: start;
}
.sidebar{
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  padding: var(--sp-6) var(--sp-5) var(--sp-6) var(--sp-5);
  border-right: 1px solid var(--line);
  background: linear-gradient(180deg,#fbfbfd,#ffffff 40%);
}
.brand{ display:flex; gap:10px; align-items:center; margin-bottom: var(--sp-6); }
.brand__mark{
  width:30px; height:30px; border-radius:9px; flex:none;
  background:
      radial-gradient(circle at 30% 30%, #7c74f0 0 45%, transparent 46%),
      linear-gradient(140deg, var(--accent) 0 55%, var(--accent2) 55% 100%);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.06);
}
.brand__name{ font-weight:680; font-size:.98rem; letter-spacing:-.01em; }
.brand__meta{ font-size:.76rem; color:var(--ink-3); letter-spacing:.02em; }

.nav__label{
  font-size:.68rem; font-weight:680; letter-spacing:.1em; text-transform:uppercase;
  color: var(--ink-3); margin: var(--sp-5) 0 var(--sp-2) 10px;
}
.nav__label:first-of-type{ margin-top:0; }
.nav a{
  display:flex; align-items:center; gap:10px;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  color: var(--ink-2);
  font-size:.9rem;
  text-decoration:none;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.nav a::before{
  content:""; width:5px; height:5px; border-radius:50%;
  background: var(--line-2); flex:none; transition: background var(--dur-fast) ease;
}
.nav a:hover{ background: var(--bg-sunk); color: var(--ink); }
.nav a[aria-current="true"]{ background: var(--accent-soft); color: var(--accent-strong); font-weight:600; }
.nav a[aria-current="true"]::before{ background: var(--accent); }

.content{ padding: var(--sp-8) var(--sp-7) 140px; max-width: var(--content-max); }
.page-head{ padding-bottom: var(--sp-6); border-bottom:1px solid var(--line); margin-bottom: var(--sp-7); }
.eyebrow{
  display:inline-flex; align-items:center; gap:7px;
  font-size:.72rem; font-weight:660; letter-spacing:.1em; text-transform:uppercase;
  color: var(--accent-strong);
  background: var(--accent-soft);
  border:1px solid var(--accent-line);
  padding:4px 10px; border-radius:999px;
}
.eyebrow::before{ content:""; width:6px; height:6px; border-radius:50%; background:var(--accent2); }
h1{ font-size: clamp(1.9rem, 1.5rem + 1.4vw, 2.6rem); margin: var(--sp-4) 0 var(--sp-3); }
.lede{ font-size:1.08rem; color: var(--ink-2); max-width: 44rem; }

/* ==========================================================================
   5. SECTIONS
   ========================================================================== */
.section{ margin-bottom: var(--sp-8); scroll-margin-top: var(--sp-5); }
.section__head{ display:flex; align-items:baseline; gap: var(--sp-3); flex-wrap:wrap; }
h2{ font-size:1.32rem; }
.section__badge{
  font-family: var(--font-mono); font-size:.7rem; font-weight:600;
  color: var(--accent2); background: var(--accent2-soft);
  border:1px solid var(--accent2-line); border-radius:999px; padding:2px 9px;
  letter-spacing:.02em;
}
.section__desc{ color: var(--ink-2); margin-top: var(--sp-3); max-width: 46rem; }
.section__desc + .demo, .section__head + .section__desc + .demo{ margin-top: var(--sp-5); }

/* Demo card */
.demo{
  margin-top: var(--sp-5);
  border:1px solid var(--line);
  border-radius: var(--radius-lg);
  background: #fff;
  box-shadow: var(--shadow-1);
  overflow: visible;
}
.demo__stage{
  padding: var(--sp-6);
  background:
    radial-gradient(circle at 1px 1px, var(--bg-sunk) 1px, transparent 0) 0 0 / 14px 14px,
    #fdfdfe;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  min-height: 96px;
  display:flex; flex-wrap:wrap; align-items:flex-start; gap: var(--sp-4);
}
.demo__caption{
  border-top:1px solid var(--line);
  background: var(--bg-soft);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  padding: 12px var(--sp-5);
  font-size:.82rem;
  color: var(--ink-2);
  display:flex; flex-wrap:wrap; align-items:center; gap:8px;
}
kbd{
  font-family: var(--font-mono); font-size:.72rem; font-weight:600;
  padding:2px 6px; border:1px solid var(--line-2); border-bottom-width:2px;
  border-radius:5px; background:#fff; color:var(--ink); white-space:nowrap;
}
.demo__caption .sep{ color: var(--line-2); }

/* ==========================================================================
   6. BUTTONS
   ========================================================================== */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  padding: 9px 16px;
  border-radius: var(--radius);
  border:1px solid var(--line-2);
  background:#fff;
  color: var(--ink);
  font-size:.9rem;
  font-weight:560;
  cursor:pointer;
  white-space:nowrap;
  transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease, transform var(--dur-fast) ease;
  box-shadow: var(--shadow-1);
}
.btn:hover{ background: var(--bg-soft); border-color: var(--ink-3); }
.btn:active{ transform: translateY(1px); }
.btn--primary{
  background: var(--accent); border-color: var(--accent); color:#fff;
  box-shadow: 0 1px 2px rgba(70,64,216,.4);
}
.btn--primary:hover{ background: var(--accent-strong); border-color: var(--accent-strong); }
.btn--danger{ background: var(--danger); border-color: var(--danger); color:#fff; }
.btn--danger:hover{ background:#8f1d17; border-color:#8f1d17; }
.btn--ghost{ background: transparent; box-shadow:none; }
.btn--ghost:hover{ background: var(--bg-sunk); }
.btn__caret{
  width:0; height:0; border-left:4px solid transparent; border-right:4px solid transparent;
  border-top:5px solid currentColor; opacity:.65; margin-left:2px;
}
.btn[aria-expanded="true"] .btn__caret{ transform: rotate(180deg); }
.btn[aria-expanded="true"]{ background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent-strong); }

/* ==========================================================================
   7. MODAL DIALOG
   ========================================================================== */
.overlay{
  position: fixed; inset:0; z-index: 60;
  display:grid; place-items:center;
  padding: var(--sp-5);
  background: rgba(18,20,28,.52);
  backdrop-filter: blur(2px);
  animation: fade var(--dur) var(--ease);
}
@keyframes fade{ from{ opacity:0 } to{ opacity:1 } }
.dialog{
  width: min(30rem, 100%);
  max-height: calc(100vh - 2 * var(--sp-5));
  overflow:auto;
  background:#fff;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-3);
  animation: pop var(--dur-slow) var(--ease);
  overscroll-behavior: contain;
}
@keyframes pop{ from{ opacity:0; transform: translateY(10px) scale(.985) } to{ opacity:1; transform:none } }
.dialog__head{
  display:flex; align-items:flex-start; gap: var(--sp-4);
  padding: var(--sp-5) var(--sp-5) var(--sp-3);
}
.dialog__head h2{ font-size:1.12rem; }
.dialog__body{ padding: 0 var(--sp-5) var(--sp-5); color: var(--ink-2); font-size:.94rem; }
.dialog__actions{
  display:flex; justify-content:flex-end; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-top:1px solid var(--line);
  background: var(--bg-soft);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.icon-btn{
  width:34px; height:34px; flex:none; display:grid; place-items:center;
  border-radius: var(--radius-sm); border:1px solid transparent; background:transparent;
  cursor:pointer; color: var(--ink-3); font-size:1.05rem; line-height:1;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.icon-btn:hover{ background: var(--bg-sunk); color: var(--ink); }
.dialog__meta{
  margin-top: var(--sp-4); padding: var(--sp-3) var(--sp-4);
  border-left:3px solid var(--accent-line); background: var(--accent-soft);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-size:.83rem; color: var(--accent-strong);
}
body.is-locked{ overflow:hidden; }

/* ==========================================================================
   8. DROPDOWN MENU
   ========================================================================== */
.menu-wrap{ position: relative; display:inline-block; }
.menu{
  position: absolute; top: calc(100% + 6px); left:0; z-index: 30;
  min-width: 15rem;
  padding: 6px;
  background:#fff;
  border:1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
  animation: menuIn var(--dur-fast) var(--ease);
}
@keyframes menuIn{ from{ opacity:0; transform: translateY(-4px) } to{ opacity:1; transform:none } }
.menu__item{
  display:flex; align-items:center; justify-content:space-between; gap: var(--sp-4);
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  font-size:.9rem;
  cursor:pointer;
  user-select:none;
  outline-offset: -2px;
}
.menu__item:hover, .menu__item[data-active="true"]{ background: var(--bg-sunk); }
.menu__item:focus{ background: var(--accent-soft); color: var(--accent-strong); }
.menu__hint{ font-family: var(--font-mono); font-size:.72rem; color: var(--ink-3); }
.menu__sep{ height:1px; margin:6px 4px; background: var(--line); }
.menu__item--danger{ color: var(--danger); }
.menu__item--danger:focus{ background:#fdecea; color:#8f1d17; }

/* ==========================================================================
   9. TABS
   ========================================================================== */
.tabs{
  border:1px solid var(--line);
  border-radius: var(--radius-lg);
  background:#fff;
  box-shadow: var(--shadow-1);
  overflow:hidden;
}
.tablist{
  display:flex; gap:2px;
  padding: 6px 6px 0;
  border-bottom:1px solid var(--line);
  background: var(--bg-soft);
  overflow-x:auto;
}
.tab{
  position: relative;
  padding: 10px 15px 12px;
  border:1px solid transparent;
  border-bottom:none;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  background: transparent;
  font-size:.9rem; font-weight:560; color: var(--ink-2);
  cursor:pointer;
  white-space:nowrap;
  transition: color var(--dur-fast) ease, background var(--dur-fast) ease;
}
.tab:hover{ color: var(--ink); background: rgba(0,0,0,.03); }
.tab[aria-selected="true"]{
  color: var(--accent-strong);
  background:#fff;
  box-shadow: inset 0 0 0 1px var(--line);
}
.tab[aria-selected="true"]::after{
  content:""; position:absolute; left:0; right:0; bottom:-1px; height:2px; background:#fff;
}
.tab::before{
  content:""; position:absolute; left:12px; right:12px; top:0; height:2px;
  background: var(--accent); border-radius:2px 2px 0 0; transform: scaleX(0);
  transition: transform var(--dur) var(--ease);
}
.tab[aria-selected="true"]::before{ transform: scaleX(1); }
.tabpanel{ padding: var(--sp-5); }
.tabpanel h3{ font-size:1rem; margin-bottom: var(--sp-2); }
.tabpanel p{ color: var(--ink-2); font-size:.94rem; max-width: 52rem; }
.tabpanel ul{ margin-top: var(--sp-3); display:grid; gap:8px; }
.tabpanel li{ position:relative; padding-left:20px; font-size:.92rem; color:var(--ink-2); }
.tabpanel li::before{
  content:""; position:absolute; left:4px; top:.6em; width:6px; height:6px; border-radius:2px;
  background: var(--accent2);
}
.code{
  margin-top: var(--sp-3);
  font-family: var(--font-mono); font-size:.8rem; line-height:1.7;
  background:#1a1c24; color:#e6e8f0;
  padding: var(--sp-4); border-radius: var(--radius);
  overflow-x:auto; white-space:pre;
}
.code .t{ color:#a8b6ff; }
.code .a{ color:#7fd8c6; }

/* ==========================================================================
   10. COMBOBOX
   ========================================================================== */
.field{ width: min(26rem, 100%); }
.field__label{ display:block; font-size:.82rem; font-weight:620; color: var(--ink-2); margin-bottom: var(--sp-2); }
.combo{ position: relative; }
.combo__input{
  width:100%;
  padding: 10px 12px;
  font: inherit;
  font-size:.95rem;
  color: var(--ink);
  background:#fff;
  border:1px solid var(--line-2);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur-fast) ease;
}
.combo__input::placeholder{ color: var(--ink-3); }
.combo__input:hover{ border-color: var(--ink-3); }
.listbox{
  position:absolute; top: calc(100% + 5px); left:0; right:0; z-index:20;
  max-height: 15.5rem; overflow-y:auto;
  padding:5px;
  background:#fff;
  border:1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
  overscroll-behavior: contain;
}
.option{
  display:flex; align-items:center; justify-content:space-between; gap: var(--sp-3);
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  font-size:.9rem;
  cursor:pointer;
}
.option[aria-selected="true"]{ background: var(--accent-soft); color: var(--accent-strong); }
.option[aria-selected="true"]::after{ content:"↵"; font-family:var(--font-mono); font-size:.75rem; opacity:.6; }
.option__empty{ padding: 10px 12px; font-size:.88rem; color: var(--ink-3); }

.pill-row{ display:flex; gap: var(--sp-2); align-items:center; flex-wrap:wrap; margin-top: var(--sp-4); }
.pill{
  display:inline-flex; align-items:center; gap:7px;
  font-size:.8rem; font-weight:560;
  padding: 4px 11px; border-radius:999px;
  background: var(--accent2-soft); color:#0a5f56; border:1px solid var(--accent2-line);
}
.pill--muted{ background: var(--bg-soft); color: var(--ink-3); border-color: var(--line); }

/* ==========================================================================
   11. ACCORDION
   ========================================================================== */
.accordion{
  border:1px solid var(--line); border-radius: var(--radius-lg);
  background:#fff; box-shadow: var(--shadow-1); overflow:hidden;
}
.acc__item + .acc__item{ border-top:1px solid var(--line); }
.acc__btn{
  width:100%;
  display:flex; align-items:center; gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  background:transparent; border:0; cursor:pointer; text-align:left;
  font-size:.96rem; font-weight:600;
  transition: background var(--dur-fast) ease;
}
.acc__btn:hover{ background: var(--bg-soft); }
.acc__btn[aria-expanded="true"]{ color: var(--accent-strong); }
.acc__chev{
  margin-left:auto; flex:none; width:10px; height:10px;
  border-right:2px solid currentColor; border-bottom:2px solid currentColor;
  transform: rotate(-45deg); opacity:.55;
  transition: transform var(--dur) var(--ease);
}
.acc__btn[aria-expanded="true"] .acc__chev{ transform: rotate(45deg); }
.acc__region{
  height:auto; overflow:hidden;
  transition: height var(--dur-slow) var(--ease), opacity var(--dur-slow) var(--ease);
}
.acc__inner{ padding: 0 var(--sp-5) var(--sp-5); color: var(--ink-2); font-size:.93rem; max-width: 56rem; }
.acc__inner p + p{ margin-top: var(--sp-3); }

/* ==========================================================================
   12. ODDS AND ENDS
   ========================================================================== */
.status-line{
  display:inline-flex; align-items:center; gap:8px;
  font-size:.85rem; color: var(--ink-2);
  padding: 7px 12px; border-radius: var(--radius);
  background:#fff; border:1px solid var(--line);
  box-shadow: var(--shadow-1);
  min-height: 38px;
}
.status-line strong{ color: var(--accent-strong); font-weight:640; }
.status-line--empty{ color: var(--ink-3); }
.toast{
  position: fixed; z-index: 70; left:50%; bottom: var(--sp-5); transform: translateX(-50%);
  background:#1a1c24; color:#fff; font-size:.88rem; font-weight:520;
  padding: 10px 18px; border-radius:999px; box-shadow: var(--shadow-3);
  animation: toastIn var(--dur) var(--ease);
}
@keyframes toastIn{ from{ opacity:0; transform: translate(-50%, 8px) } to{ opacity:1; transform: translate(-50%,0) } }
.footer{
  margin-top: var(--sp-8); padding-top: var(--sp-5); border-top:1px solid var(--line);
  color: var(--ink-3); font-size:.85rem;
}

/* ==========================================================================
   13. RESPONSIVE (incl. 200% zoom comfort) & REDUCED MOTION
   ========================================================================== */
@media (max-width: 56rem){
  .app{ grid-template-columns: 1fr; }
  .sidebar{
    position: static; height:auto; overflow:visible;
    border-right:0; border-bottom:1px solid var(--line);
    padding: var(--sp-5);
  }
  .nav ul{ display:flex; flex-wrap:wrap; gap:4px; }
  .nav a{ padding:6px 12px; background: var(--bg-soft); }
  .content{ padding: var(--sp-6) var(--sp-5) 120px; }
  .demo__stage{ padding: var(--sp-5); }
}
@media (max-width: 30rem){
  .demo__stage{ padding: var(--sp-4); }
  .btn{ width:auto; }
}
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}
</style>
</head>
<body>
<a class="skip-link" href="#main">Skip to main content</a>

<div class="app" id="app">
  <aside class="sidebar">
    <div class="brand">
      <span class="brand__mark" aria-hidden="true"></span>
      <span>
        <span class="brand__name">Prism UI</span><br>
        <span class="brand__meta">v1.0 · accessible primitives</span>
      </span>
    </div>
    <nav class="nav" aria-label="Component index">
      <p class="nav__label" id="nav-components">Components</p>
      <ul aria-labelledby="nav-components">
        <li><a href="#modal">Modal dialog</a></li>
        <li><a href="#menu">Dropdown menu</a></li>
        <li><a href="#tabs">Tabs</a></li>
        <li><a href="#combobox">Combobox</a></li>
        <li><a href="#accordion">Accordion</a></li>
      </ul>
      <p class="nav__label" id="nav-notes">Notes</p>
      <ul aria-labelledby="nav-notes">
        <li><a href="#contract">Keyboard contract</a></li>
      </ul>
    </nav>
  </aside>

  <main class="content" id="main" tabindex="-1">
    <header class="page-head">
      <p class="eyebrow">Component library</p>
      <h1>Accessible by construction</h1>
      <p class="lede">
        Five interaction patterns built to the WAI-ARIA Authoring Practices. Every widget below
        is fully operable with a keyboard, keeps its ARIA state in sync with what you see, and
        announces changes to assistive technology. Unplug your mouse and try them.
      </p>
    </header>

    <!-- ==================================================================
         1. MODAL DIALOG
         ================================================================== -->
    <section class="section" id="modal" aria-labelledby="modal-h">
      <div class="section__head">
        <h2 id="modal-h">Modal dialog</h2>
        <span class="section__badge">role="dialog" · aria-modal="true"</span>
      </div>
      <p class="section__desc">
        A contained, blocking surface. While it is open the rest of the page is marked
        <code>inert</code>, so neither Tab nor a screen reader cursor can escape it.
      </p>
      <div class="demo">
        <div class="demo__stage">
          <button class="btn btn--primary" id="modal-trigger" aria-haspopup="dialog">Remove project…</button>
        </div>
        <p class="demo__caption">
          <kbd>Tab</kbd> cycles inside the dialog
          <span class="sep">·</span> <kbd>Shift</kbd>+<kbd>Tab</kbd> reverses
          <span class="sep">·</span> <kbd>Esc</kbd> closes
          <span class="sep">·</span> focus returns to the trigger
        </p>
      </div>
    </section>

    <!-- ==================================================================
         2. DROPDOWN MENU
         ================================================================== -->
    <section class="section" id="menu" aria-labelledby="menu-h">
      <div class="section__head">
        <h2 id="menu-h">Dropdown menu</h2>
        <span class="section__badge">aria-haspopup="menu"</span>
      </div>
      <p class="section__desc">
        A list of actions anchored to a button. Focus is roving: the menu owns exactly one
        tab stop, and arrow keys move the highlight inside it.
      </p>
      <div class="demo">
        <div class="demo__stage">
          <div class="menu-wrap" id="menu-wrap">
            <button class="btn" id="menu-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="menu-list">
              Project actions <span class="btn__caret" aria-hidden="true"></span>
            </button>
            <ul class="menu" id="menu-list" role="menu" aria-labelledby="menu-btn" hidden>
              <li class="menu__item" role="menuitem" tabindex="-1" data-action="New file from template">New file from template<span class="menu__hint">⌘N</span></li>
              <li class="menu__item" role="menuitem" tabindex="-1" data-action="Duplicate project">Duplicate project<span class="menu__hint">⌘D</span></li>
              <li class="menu__item" role="menuitem" tabindex="-1" data-action="Rename project">Rename project<span class="menu__hint">F2</span></li>
              <li class="menu__item" role="menuitem" tabindex="-1" data-action="Export as JSON">Export as JSON</li>
              <li role="separator" class="menu__sep"></li>
              <li class="menu__item menu__item--danger" role="menuitem" tabindex="-1" data-action="Delete project">Delete project</li>
            </ul>
          </div>
          <p class="status-line status-line--empty" id="menu-status">No action yet</p>
        </div>
        <p class="demo__caption">
          <kbd>Enter</kbd> or <kbd>↓</kbd> opens
          <span class="sep">·</span> <kbd>↑</kbd><kbd>↓</kbd> navigate
          <span class="sep">·</span> <kbd>Home</kbd>/<kbd>End</kbd> jump
          <span class="sep">·</span> <kbd>Enter</kbd> activates
          <span class="sep">·</span> <kbd>Esc</kbd> closes
        </p>
      </div>
    </section>

    <!-- ==================================================================
         3. TABS
         ================================================================== -->
    <section class="section" id="tabs" aria-labelledby="tabs-h">
      <div class="section__head">
        <h2 id="tabs-h">Tabs</h2>
        <span class="section__badge">role="tablist"</span>
      </div>
      <p class="section__desc">
        Only the selected tab is in the tab order — arrows move between tabs and activate them,
        and panels that are not shown carry the <code>hidden</code> attribute rather than being
        merely pushed off-screen.
      </p>
      <div class="tabs demo">
        <div class="tablist" role="tablist" aria-labelledby="tabs-h">
          <button class="tab" id="tab-anatomy" role="tab" aria-selected="true" aria-controls="panel-anatomy" tabindex="0">Anatomy</button>
          <button class="tab" id="tab-keyboard" role="tab" aria-selected="false" aria-controls="panel-keyboard" tabindex="-1">Keyboard</button>
          <button class="tab" id="tab-markup" role="tab" aria-selected="false" aria-controls="panel-markup" tabindex="-1">Markup</button>
        </div>
        <div class="tabpanel" id="panel-anatomy" role="tabpanel" aria-labelledby="tab-anatomy" tabindex="0">
          <h3>What the pattern is made of</h3>
          <p>Three parts, all of them labelled: the trigger, the surface it controls, and the state that links them.</p>
          <ul>
            <li>The trigger carries <code>aria-expanded</code> so the state is readable without sight.</li>
            <li>The controlled region is referenced by <code>aria-controls</code>, which must resolve.</li>
            <li>Hover, focus and selected styles are deliberately distinct.</li>
          </ul>
        </div>
        <div class="tabpanel" id="panel-keyboard" role="tabpanel" aria-labelledby="tab-keyboard" tabindex="0" hidden>
          <h3>Keyboard behaviour</h3>
          <p>Arrow keys move and activate in one step (automatic activation). The tab row keeps a single tab stop.</p>
          <ul>
            <li><kbd>←</kbd> / <kbd>→</kbd> move between tabs, wrapping at both ends.</li>
            <li><kbd>Home</kbd> and <kbd>End</kbd> jump to the first and last tab.</li>
            <li><kbd>Tab</kbd> moves into the visible panel, whose content is then reachable.</li>
          </ul>
        </div>
        <div class="tabpanel" id="panel-markup" role="tabpanel" aria-labelledby="tab-markup" tabindex="0" hidden>
          <h3>Wiring, in full</h3>
          <p>Every id below resolves to a real element on this page — that is asserted in the self-test.</p>
          <pre class="code"><span class="t">&lt;div</span> role=<span class="a">"tablist"</span> <span class="t">aria-label</span>=<span class="a">"…"</span><span class="t">&gt;</span>
  <span class="t">&lt;button</span> role=<span class="a">"tab"</span> id=<span class="a">"tab-anatomy"</span>
          aria-selected=<span class="a">"true"</span>
          aria-controls=<span class="a">"panel-anatomy"</span>
          tabindex=<span class="a">"0"</span><span class="t">&gt;</span>Anatomy<span class="t">&lt;/button&gt;</span>
<span class="t">&lt;/div&gt;</span>
<span class="t">&lt;div</span> role=<span class="a">"tabpanel"</span> id=<span class="a">"panel-anatomy"</span>
     aria-labelledby=<span class="a">"tab-anatomy"</span> tabindex=<span class="a">"0"</span><span class="t">&gt;</span>…<span class="t">&lt;/div&gt;</span></pre>
        </div>
      </div>
      <p class="demo__caption" style="border:1px solid var(--line); border-radius:var(--radius-lg); margin-top: var(--sp-4); background:var(--bg-soft);">
        <kbd>←</kbd><kbd>→</kbd> switch tabs
        <span class="sep">·</span> <kbd>Home</kbd>/<kbd>End</kbd> first / last
        <span class="sep">·</span> <kbd>Tab</kbd> enters the open panel
      </p>
    </section>

    <!-- ==================================================================
         4. COMBOBOX
         ================================================================== -->
    <section class="section" id="combobox" aria-labelledby="combobox-h">
      <div class="section__head">
        <h2 id="combobox-h">Combobox</h2>
        <span class="section__badge">aria-activedescendant</span>
      </div>
      <p class="section__desc">
        An editable text field with a filtered listbox. The input keeps and owns DOM focus;
        the highlighted option is communicated with <code>aria-activedescendant</code>, so the
        listbox itself is never focusable.
      </p>
      <div class="demo">
        <div class="demo__stage">
          <div class="field">
            <label class="field__label" id="fruit-label" for="fruit-input">Preferred fruit</label>
            <div class="combo">
              <input class="combo__input" id="fruit-input" type="text"
                     role="combobox" aria-autocomplete="list"
                     aria-expanded="false" aria-controls="fruit-listbox"
                     aria-labelledby="fruit-label"
                     autocomplete="off" spellcheck="false" placeholder="Start typing, e.g. “man”">
              <ul class="listbox" id="fruit-listbox" role="listbox" aria-labelledby="fruit-label" hidden></ul>
            </div>
            <div class="pill-row">
              <span class="pill" id="fruit-selection" hidden>Selected: <strong id="fruit-selection-value"></strong></span>
              <span class="pill pill--muted" id="fruit-count">28 matches</span>
            </div>
          </div>
        </div>
        <p class="demo__caption">
          Type to filter <span class="sep">·</span> <kbd>↓</kbd><kbd>↑</kbd> move the highlight
          <span class="sep">·</span> <kbd>Enter</kbd> accepts
          <span class="sep">·</span> <kbd>Esc</kbd> reverts and closes
        </p>
      </div>
    </section>

    <!-- ==================================================================
         5. ACCORDION
         ================================================================== -->
    <section class="section" id="accordion" aria-labelledby="accordion-h">
      <div class="section__head">
        <h2 id="accordion-h">Accordion</h2>
        <span class="section__badge">disclosure group</span>
      </div>
      <p class="section__desc">
        Independent disclosures — several may be open at once. Regions animate on height, and
        <code>hidden</code> is only applied once the closing animation has finished, so
        assistive technology never sees a half-collapsed region.
      </p>
      <div class="accordion demo">
        <div class="acc__item">
          <h3>
            <button class="acc__btn" id="acc-btn-1" aria-expanded="true" aria-controls="acc-region-1">
              Installation <span class="acc__chev" aria-hidden="true"></span>
            </button>
          </h3>
          <div class="acc__region" id="acc-region-1" role="region" aria-labelledby="acc-btn-1">
            <div class="acc__inner">
              <p>Copy the primitives you need. There is no runtime dependency: each component is a single
                 behavioural module plus its stylesheet layer.</p>
              <p>Nothing is fetched at runtime, so the library works in air-gapped builds, offline docs and
                 print stylesheets alike.</p>
            </div>
          </div>
        </div>
        <div class="acc__item">
          <h3>
            <button class="acc__btn" id="acc-btn-2" aria-expanded="false" aria-controls="acc-region-2">
              Focus management <span class="acc__chev" aria-hidden="true"></span>
            </button>
          </h3>
          <div class="acc__region" id="acc-region-2" role="region" aria-labelledby="acc-btn-2" hidden>
            <div class="acc__inner">
              <p>The one rule: focus is never destroyed. Closing a surface returns focus to whatever opened
                 it, and every surface that traps focus offers a documented way out.</p>
              <p>Focus indicators here are a 3px ring with a 2px offset — visible against both the tinted stage
                 background and white cards.</p>
            </div>
          </div>
        </div>
        <div class="acc__item">
          <h3>
            <button class="acc__btn" id="acc-btn-3" aria-expanded="false" aria-controls="acc-region-3">
              Motion &amp; preferences <span class="acc__chev" aria-hidden="true"></span>
            </button>
          </h3>
          <div class="acc__region" id="acc-region-3" role="region" aria-labelledby="acc-btn-3" hidden>
            <div class="acc__inner">
              <p>Every animation is expressed as a duration token, and the whole page collapses to
                 near-zero durations under <code>prefers-reduced-motion: reduce</code>.</p>
              <p>Content stays visible in that mode — the region is opened and closed instantly instead of
                 sliding.</p>
            </div>
          </div>
        </div>
      </div>
      <p class="demo__caption" style="border:1px solid var(--line); border-radius:var(--radius-lg); margin-top: var(--sp-4); background:var(--bg-soft);">
        <kbd>Tab</kbd> to a header <span class="sep">·</span> <kbd>Enter</kbd> / <kbd>Space</kbd> toggles
        <span class="sep">·</span> multiple panels may stay open
      </p>
    </section>

    <!-- ==================================================================
         KEYBOARD CONTRACT SUMMARY
         ================================================================== -->
    <section class="section" id="contract" aria-labelledby="contract-h" style="margin-bottom:0">
      <div class="section__head"><h2 id="contract-h">Keyboard contract</h2></div>
      <p class="section__desc">
        The short version, if you only remember one line per component:
      </p>
      <div class="demo">
        <div class="demo__stage" style="display:block">
          <ul style="display:grid; gap:10px; font-size:.93rem; color:var(--ink-2)">
            <li><strong>Dialog</strong> — Tab is trapped · Esc closes · focus returns to the trigger.</li>
            <li><strong>Menu</strong> — ↓ opens and focuses the first item · ↑↓ wrap · Esc closes and refocuses the button.</li>
            <li><strong>Tabs</strong> — one tab stop · ←→ activate · hidden panels are <code>hidden</code>.</li>
            <li><strong>Combobox</strong> — typing filters · ↑↓ moves <code>aria-activedescendant</code> · Esc reverts.</li>
            <li><strong>Accordion</strong> — Enter/Space toggles · height animates · <code>hidden</code> lands at the end.</li>
          </ul>
        </div>
      </div>
      <p class="footer">Built with hand-written CSS and vanilla JavaScript. No frameworks, no external assets.</p>
    </section>
  </main>
</div>

<!-- Modal lives outside the app shell so the shell can be made inert -->
<div class="overlay" id="overlay" hidden>
  <div class="dialog" role="dialog" aria-modal="true" id="dialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
    <div class="dialog__head">
      <h2 id="dialog-title">Remove this project?</h2>
      <button class="icon-btn" id="dialog-close" type="button" aria-label="Close dialog" style="margin-left:auto">✕</button>
    </div>
    <div class="dialog__body">
      <p id="dialog-desc">
        “Atlas Redesign” and its 42 files will be moved to the trash. Members keep read access
        for 30 days, after which the project is permanently deleted.
      </p>
      <p class="dialog__meta">While this dialog is open, the rest of the page is inert: Tab cannot reach it.</p>
    </div>
    <div class="dialog__actions">
      <button class="btn btn--ghost" id="dialog-cancel" type="button" data-autofocus>Cancel</button>
      <button class="btn btn--danger" id="dialog-confirm" type="button">Remove project</button>
    </div>
  </div>
</div>

<p class="sr-only" role="status" aria-live="polite" id="live-region"></p>

<script>
/* ==========================================================================
   A. CONSTANTS & SMALL UTILITIES
   ========================================================================== */
const MOTION = {
  fast: 120,
  base: 200,
  slow: 260
};
const KEY = {
  ENTER: 'Enter', SPACE: ' ', ESC: 'Escape',
  UP: 'ArrowUp', DOWN: 'ArrowDown', LEFT: 'ArrowLeft', RIGHT: 'ArrowRight',
  HOME: 'Home', END: 'End', TAB: 'Tab'
};

const $  = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));

const prefersReducedMotion = () =>
  window.matchMedia('(prefers-reduced-motion: reduce)').matches;

/** Clamp/wrap an index while stepping through a list. */
function stepIndex(current, delta, length, wrap = true) {
  if (length <= 0) return -1;
  let next = current + delta;
  if (wrap) return (next % length + length) % length;
  return Math.max(0, Math.min(length - 1, next));
}

/** Polite announcement for state changes that aren't otherwise obvious. */
const liveRegion = $('#live-region');
let announceTimer = 0;
function announce(message) {
  window.clearTimeout(announceTimer);
  liveRegion.textContent = '';
  announceTimer = window.setTimeout(() => { liveRegion.textContent = message; }, 60);
}

/** Transient toast, purely visual feedback for commits. */
let toastEl = null, toastTimer = 0;
function toast(message) {
  if (toastEl) toastEl.remove();
  toastEl = document.createElement('div');
  toastEl.className = 'toast';
  toastEl.setAttribute('aria-hidden', 'true'); // the live region carries the message
  toastEl.textContent = message;
  document.body.appendChild(toastEl);
  window.clearTimeout(toastTimer);
  toastTimer = window.setTimeout(() => { toastEl.remove(); toastEl = null; }, 2600);
}

/** Wait for a height transition to finish (with a safety net). */
function onHeightTransitionEnd(el, callback) {
  let settled = false;
  const done = () => {
    if (settled) return;
    settled = true;
    el.removeEventListener('transitionend', handler);
    callback();
  };
  function handler(event) {
    if (event.target !== el || event.propertyName !== 'height') return;
    done();
  }
  el.addEventListener('transitionend', handler);
  window.setTimeout(done, MOTION.slow + 180);
}

/* ==========================================================================
   B. SHARED HELPER — FOCUS TRAP
   Generic modal trap: keeps Tab / Shift+Tab cycling inside a container.
   ========================================================================== */
const FOCUSABLE_SELECTOR = [
  'a[href]', 'button:not([disabled])', 'input:not([disabled]):not([type="hidden"])',
  'select:not([disabled])', 'textarea:not([disabled])',
  '[tabindex]:not([tabindex="-1"])'
].join(',');

function createFocusTrap(container) {
  function focusables() {
    return $$(FOCUSABLE_SELECTOR, container)
      .filter((el) => el.getClientRects().length > 0 && el.getAttribute('aria-hidden') !== 'true');
  }

  function onKeydown(event) {
    if (event.key !== KEY.TAB) return;
    const items = focusables();
    if (items.length === 0) { event.preventDefault(); return; }

    const first = items[0];
    const last = items[items.length - 1];
    const active = document.activeElement;

    if (!container.contains(active)) {
      event.preventDefault();
      (event.shiftKey ? last : first).focus();
      return;
    }
    if (event.shiftKey && active === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && active === last) {
      event.preventDefault();
      first.focus();
    }
  }

  return {
    focusables,
    activate() { document.addEventListener('keydown', onKeydown, true); },
    deactivate() { document.removeEventListener('keydown', onKeydown, true); }
  };
}

/* ==========================================================================
   C. SHARED HELPER — ARIA-ACTIVEDESCENDANT MANAGER
   Renders a listbox, owns the highlight index, and keeps
   `aria-activedescendant` on the owning input in sync. The listbox itself
   never receives focus.
   ========================================================================== */
function createActiveDescendantManager({ input, listbox, idPrefix, optionClass = 'option' }) {
  let items = [];
  let activeIndex = -1;

  const optionId = (i) => `${idPrefix}-opt-${i}`;
  const nodes = () => Array.from(listbox.children);

  function render(nextItems, emptyMessage) {
    items = nextItems;
    listbox.textContent = '';
    items.forEach((item, i) => {
      const li = document.createElement('li');
      li.id = optionId(i);
      li.className = optionClass;
      li.setAttribute('role', 'option');
      li.setAttribute('aria-selected', 'false');
      li.textContent = item.label;
      listbox.appendChild(li);
    });
    if (items.length === 0 && emptyMessage) {
      const empty = document.createElement('li');
      empty.className = 'option__empty';
      empty.setAttribute('role', 'presentation');
      empty.textContent = emptyMessage;
      listbox.appendChild(empty);
    }
    setActive(-1);
  }

  function setActive(index, { scroll = true } = {}) {
    if (items.length === 0) { clear(); return; }
    activeIndex = Math.max(0, Math.min(items.length - 1, index));
    nodes().forEach((node, i) => {
      if (node.getAttribute('role') === 'option') {
        node.setAttribute('aria-selected', i === activeIndex ? 'true' : 'false');
      }
    });
    input.setAttribute('aria-activedescendant', optionId(activeIndex));
    if (scroll) {
      const node = document.getElementById(optionId(activeIndex));
      if (node) node.scrollIntoView({ block: 'nearest' });
    }
  }

  function clear() {
    activeIndex = -1;
    input.removeAttribute('aria-activedescendant');
    nodes().forEach((node) => {
      if (node.getAttribute('role') === 'option') node.setAttribute('aria-selected', 'false');
    });
  }

  return {
    render,
    setActive,
    clear,
    move(delta) { if (items.length) setActive(stepIndex(activeIndex, delta, items.length, true)); },
    jump(key) { if (items.length) setActive(key === KEY.HOME ? 0 : items.length - 1); },
    itemAt(i) { return items[i]; },
    activeIndex() { return activeIndex; },
    count() { return items.length; }
  };
}

/* ==========================================================================
   D. COMPONENT — MODAL DIALOG
   ========================================================================== */
(function initModal() {
  const overlay = $('#overlay');
  const dialog = $('#dialog');
  const appShell = $('#app');
  const trigger = $('#modal-trigger');
  const cancel = $('#dialog-cancel');
  const confirm = $('#dialog-confirm');
  const closeBtn = $('#dialog-close');
  const trap = createFocusTrap(dialog);
  let opener = null;

  function open() {
    opener = document.activeElement instanceof HTMLElement ? document.activeElement : trigger;
    overlay.hidden = false;
    appShell.setAttribute('inert', '');   // page behind is not focusable / not announced
    document.body.classList.add('is-locked');
    trap.activate();
    const initial = dialog.querySelector('[data-autofocus]') || trap.focusables()[0];
    if (initial) initial.focus();
  }

  function close() {
    if (overlay.hidden) return;
    overlay.hidden = true;
    appShell.removeAttribute('inert');
    document.body.classList.remove('is-locked');
    trap.deactivate();
    (opener && opener.isConnected ? opener : trigger).focus();  // focus returns to the trigger
  }

  trigger.addEventListener('click', open);
  cancel.addEventListener('click', close);
  closeBtn.addEventListener('click', close);
  confirm.addEventListener('click', () => {
    announce('Project moved to the trash.');
    toast('Project moved to the trash');
    close();
  });

  // Escape closes; a click on the backdrop (outside the dialog) closes too.
  document.addEventListener('keydown', (event) => {
    if (event.key === KEY.ESC && !overlay.hidden) {
      event.preventDefault();
      close();
    }
  });
  overlay.addEventListener('mousedown', (event) => {
    if (event.target === overlay) close();
  });

  window.__modal = { open, close, isOpen: () => !overlay.hidden };
})();

/* ==========================================================================
   E. COMPONENT — DROPDOWN MENU
   Roving focus inside the menu; one tab stop lives on the trigger button.
   ========================================================================== */
(function initMenu() {
  const wrap = $('#menu-wrap');
  const button = $('#menu-btn');
  const menu = $('#menu-list');
  const status = $('#menu-status');
  const items = $$('[role="menuitem"]', menu);
  let open = false;

  function openMenu(focusIndex) {
    if (!open) {
      menu.hidden = false;
      button.setAttribute('aria-expanded', 'true');
      open = true;
    }
    if (focusIndex !== null && focusIndex !== undefined) items[focusIndex].focus();
  }

  function closeMenu({ refocus = false } = {}) {
    if (!open) return;
    menu.hidden = true;
    button.setAttribute('aria-expanded', 'false');
    items.forEach((item) => item.removeAttribute('data-active'));
    open = false;
    if (refocus) button.focus();
  }

  function activate(item) {
    const action = item.dataset.action;
    status.classList.remove('status-line--empty');
    status.innerHTML = 'Last action: <strong></strong>';
    status.querySelector('strong').textContent = action;
    announce(`${action} activated.`);
    toast(action);
    closeMenu({ refocus: true });   // focus returns to the trigger
  }

  // Click / Enter / Space on the button (buttons emit click for all three).
  button.addEventListener('click', () => (open ? closeMenu({ refocus: false }) : openMenu(null)));
  button.addEventListener('keydown', (event) => {
    if (event.key === KEY.DOWN) {
      event.preventDefault();
      openMenu(0);
    } else if (event.key === KEY.UP) {
      event.preventDefault();
      openMenu(items.length - 1);
    } else if (event.key === KEY.ESC && open) {
      event.preventDefault();
      closeMenu({ refocus: true });
    } else if (event.key === KEY.TAB && open) {
      closeMenu({ refocus: false });   // let focus move on naturally
    }
  });

  menu.addEventListener('keydown', (event) => {
    const current = items.indexOf(document.activeElement);
    switch (event.key) {
      case KEY.DOWN: event.preventDefault(); items[stepIndex(current, 1, items.length)].focus(); break;
      case KEY.UP:   event.preventDefault(); items[stepIndex(current, -1, items.length)].focus(); break;
      case KEY.HOME: event.preventDefault(); items[0].focus(); break;
      case KEY.END:  event.preventDefault(); items[items.length - 1].focus(); break;
      case KEY.ESC:  event.preventDefault(); closeMenu({ refocus: true }); break;
      case KEY.TAB:  closeMenu({ refocus: false }); break;   // focus moves on naturally
      case KEY.ENTER:
      case KEY.SPACE:
        if (current >= 0) { event.preventDefault(); activate(items[current]); }
        break;
      default: break;
    }
  });

  items.forEach((item) => {
    item.addEventListener('click', () => activate(item));
    item.addEventListener('pointerenter', () => {
      items.forEach((other) => other.removeAttribute('data-active'));
      item.setAttribute('data-active', 'true');
    });
    item.addEventListener('pointerleave', () => item.removeAttribute('data-active'));
  });

  // Clicking anywhere outside dismisses the menu.
  document.addEventListener('pointerdown', (event) => {
    if (open && !wrap.contains(event.target)) closeMenu({ refocus: false });
  });
  // Tabbing away from an open menu dismisses it, wherever focus currently sits.
  document.addEventListener('keydown', (event) => {
    if (open && event.key === KEY.TAB) closeMenu({ refocus: false });
  }, true);
  window.__menu = { openMenu, closeMenu, isOpen: () => open, items };
})();

/* ==========================================================================
   F. COMPONENT — TABS
   Automatic activation + roving tabindex: exactly one tab stop in the tablist.
   ========================================================================== */
(function initTabs() {
  $$('[role="tablist"]').forEach((tablist) => {
    const tabs = $$('[role="tab"]', tablist);
    const panels = tabs.map((tab) => document.getElementById(tab.getAttribute('aria-controls')));

    function select(index, { moveFocus = false } = {}) {
      tabs.forEach((tab, i) => {
        const isSelected = i === index;
        tab.setAttribute('aria-selected', isSelected ? 'true' : 'false');
        tab.tabIndex = isSelected ? 0 : -1;   // exactly one tab in the page tab order
        panels[i].hidden = !isSelected;       // hidden, not merely off-screen
      });
      if (moveFocus) tabs[index].focus();
    }

    tabs.forEach((tab, i) => {
      tab.addEventListener('click', () => select(i));
      tab.addEventListener('keydown', (event) => {
        switch (event.key) {
          case KEY.LEFT:  event.preventDefault(); select(stepIndex(i, -1, tabs.length), { moveFocus: true }); break;
          case KEY.RIGHT: event.preventDefault(); select(stepIndex(i, 1, tabs.length), { moveFocus: true }); break;
          case KEY.HOME:  event.preventDefault(); select(0, { moveFocus: true }); break;
          case KEY.END:   event.preventDefault(); select(tabs.length - 1, { moveFocus: true }); break;
          default: break;
        }
      });
    });

    tablist.__select = select;
  });
})();

/* ==========================================================================
   G. COMPONENT — COMBOBOX (autocomplete)
   ========================================================================== */
(function initCombobox() {
  const input = $('#fruit-input');
  const listbox = $('#fruit-listbox');
  const countPill = $('#fruit-count');
  const selectionPill = $('#fruit-selection');
  const selectionValue = $('#fruit-selection-value');

  const OPTIONS = [
    'Apple', 'Apricot', 'Avocado', 'Banana', 'Blackberry', 'Blueberry', 'Cherry',
    'Coconut', 'Date', 'Elderberry', 'Fig', 'Grape', 'Guava', 'Kiwi', 'Lemon',
    'Lychee', 'Mango', 'Nectarine', 'Orange', 'Papaya', 'Peach', 'Pear',
    'Pineapple', 'Plum', 'Pomegranate', 'Raspberry', 'Strawberry', 'Watermelon'
  ].map((label) => ({ label, value: label.toLowerCase() }));

  const manager = createActiveDescendantManager({
    input, listbox, idPrefix: 'fruit'
  });

  let isOpen = false;
  let committedValue = '';     // last accepted value, restored on Escape
  let selectedOption = null;
  let matches = filter('');    // start with the full list

  function filter(query) {
    const q = query.trim().toLowerCase();
    if (!q) return OPTIONS.slice();
    return OPTIONS.filter((opt) => opt.value.includes(q));
  }

  function syncCount() {
    countPill.textContent = `${matches.length} ${matches.length === 1 ? 'match' : 'matches'}`;
  }

  function openList({ highlightFirst = true } = {}) {
    matches = filter(input.value);
    manager.render(matches, 'No matching fruit');
    isOpen = true;
    listbox.hidden = false;
    input.setAttribute('aria-expanded', 'true');
    syncCount();
    if (highlightFirst && matches.length) manager.setActive(0);
  }

  function closeList() {
    if (!isOpen) return;
    isOpen = false;
    listbox.hidden = true;
    input.setAttribute('aria-expanded', 'false');
    manager.clear();
  }

  function choose(index) {
    const option = manager.itemAt(index);
    if (!option) return;
    input.value = option.label;
    committedValue = option.label;
    selectedOption = option;
    selectionValue.textContent = option.label;
    selectionPill.hidden = false;
    closeList();
    announce(`${option.label} selected. ${matches.length} ${matches.length === 1 ? 'option' : 'options'} were available.`);
  }

  input.addEventListener('input', () => {
    matches = filter(input.value);
    syncCount();
    if (!isOpen) openList();
    else {
      manager.render(matches, 'No matching fruit');
      if (matches.length) manager.setActive(0);
      else manager.clear();
    }
  });

  input.addEventListener('focus', () => {
    if (!isOpen) openList();
  });

  input.addEventListener('keydown', (event) => {
    switch (event.key) {
      case KEY.DOWN:
        event.preventDefault();
        if (!isOpen) openList();
        else if (manager.activeIndex() === -1 && manager.count()) manager.setActive(0);
        else manager.move(1);
        break;
      case KEY.UP:
        event.preventDefault();
        if (!isOpen) openList({ highlightFirst: false });
        else manager.move(-1);
        break;
      case KEY.HOME:
        if (isOpen) { event.preventDefault(); manager.jump(KEY.HOME); }
        break;
      case KEY.END:
        if (isOpen) { event.preventDefault(); manager.jump(KEY.END); }
        break;
      case KEY.ENTER:
        event.preventDefault();
        if (isOpen && manager.activeIndex() > -1) choose(manager.activeIndex());
        else { committedValue = input.value; announce(`${input.value} entered.`); closeList(); }
        break;
      case KEY.ESC:
        if (isOpen) {
          event.preventDefault();
          closeList();
          input.value = committedValue;   // revert the edit
          announce('Selection cancelled. Restored ' + (committedValue || 'empty value') + '.');
        }
        break;
      case KEY.TAB:
        closeList();                       // focus moves on naturally
        break;
      default: break;
    }
  });

  // Keep DOM focus in the input while picking with a mouse.
  listbox.addEventListener('pointerdown', (event) => event.preventDefault());
  listbox.addEventListener('click', (event) => {
    const option = event.target.closest('[role="option"]');
    if (!option) return;
    choose(Array.from(listbox.children).indexOf(option));
  });
  listbox.addEventListener('pointermove', (event) => {
    const option = event.target.closest('[role="option"]');
    if (!option) return;
    const index = Array.from(listbox.children).indexOf(option);
    if (index > -1) manager.setActive(index, { scroll: false });
  });

  input.addEventListener('blur', closeList);

  syncCount();
  window.__combobox = {
    manager, input, listbox,
    isOpen: () => isOpen,
    openList, closeList, choose,
    selected: () => selectedOption
  };
})();

/* ==========================================================================
   H. COMPONENT — ACCORDION (disclosure group)
   Height animates; `hidden` is applied only once closing has finished.
   ========================================================================== */
(function initAccordion() {
  $$('.acc__btn').forEach((button) => {
    const region = document.getElementById(button.getAttribute('aria-controls'));
    if (!region) return;

    function expand() {
      button.setAttribute('aria-expanded', 'true');
      region.hidden = false;                  // must be visible to be measurable
      if (prefersReducedMotion()) {
        region.style.height = ''; region.style.opacity = '';
        return;
      }
      const target = region.scrollHeight;
      region.style.height = '0px';
      region.style.opacity = '0';
      void region.offsetHeight;               // flush so the transition starts from 0
      requestAnimationFrame(() => {
        region.style.height = target + 'px';
        region.style.opacity = '1';
      });
      onHeightTransitionEnd(region, () => {
        region.style.height = 'auto';         // never trap content in a fixed height
        region.style.opacity = '';
      });
    }

    function collapse() {
      button.setAttribute('aria-expanded', 'false');
      if (prefersReducedMotion()) {
        region.hidden = true;
        region.style.height = ''; region.style.opacity = '';
        return;
      }
      region.style.height = region.scrollHeight + 'px';
      region.style.opacity = '1';
      void region.offsetHeight;
      requestAnimationFrame(() => {
        region.style.height = '0px';
        region.style.opacity = '0';
      });
      onHeightTransitionEnd(region, () => {
        region.hidden = true;                 // hide only at the very end
        region.style.height = '';
        region.style.opacity = '';
      });
    }

    button.addEventListener('click', () => {
      const isOpen = button.getAttribute('aria-expanded') === 'true';
      if (isOpen) collapse(); else expand();
    });
  });
})();

/* ==========================================================================
   I. SIDEBAR — current-section highlighting
   ========================================================================== */
(function initScrollSpy() {
  const links = $$('.nav a[href^="#"]');
  const sections = links
    .map((link) => document.getElementById(link.getAttribute('href').slice(1)))
    .filter(Boolean);

  function update() {
    const marker = window.innerHeight * 0.28;
    let current = sections[0];
    sections.forEach((section) => {
      if (section.getBoundingClientRect().top <= marker) current = section;
    });
    links.forEach((link) => {
      const match = link.getAttribute('href') === '#' + current.id;
      if (match) link.setAttribute('aria-current', 'true');
      else link.removeAttribute('aria-current');
    });
  }

  let ticking = false;
  window.addEventListener('scroll', () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => { update(); ticking = false; });
  }, { passive: true });
  window.addEventListener('resize', update);
  update();
})();

/* ==========================================================================
   J. SELF-CHECK — every ARIA relationship must resolve to a real element
   ========================================================================== */
(function validateAriaRelationships() {
  const attributes = ['aria-controls', 'aria-labelledby', 'aria-describedby', 'aria-activedescendant'];
  const dangling = [];
  attributes.forEach((attr) => {
    $$(`[${attr}]`).forEach((el) => {
      el.getAttribute(attr).split(/\s+/).filter(Boolean).forEach((id) => {
        if (!document.getElementById(id)) dangling.push(`${attr}="${id}" on #${el.id || el.tagName}`);
      });
    });
  });
  window.__ariaReport = { dangling, ok: dangling.length === 0 };
  if (dangling.length) console.warn('Dangling ARIA references:', dangling);
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"f40bda39-d265-4602-86f9-60d6940dce8a","tokensIn":2391929,"tokensOut":54779,"tokensTotal":2446708,"cost":0.04564932600000001,"turns":37,"toolCalls":37,"failedToolCalls":0,"timestamp":"2026-09-10T17:56:08.766Z"} -->