← Accessible Components results

Accessible Components

Qwen 3.8 27B GGUF · typescript

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 69,687 bytes · SHA-256 4bafdb31e8ce
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Clarity UI — Accessible Component Showcase</title>
<style>
/* ── css: base ── */
/* ============ Clarity UI — design tokens & base ============ */
:root {
  /* palette — ink + indigo, on warm paper */
  --ink: #1c2433;
  --ink-2: #4c566c;
  --ink-3: #7a8398;
  --accent: #3653d9;
  --accent-deep: #26379e;
  --accent-soft: #e9edfb;
  --accent-soft-2: #f1f4fe;
  --paper: #f5f6fa;
  --card: #ffffff;
  --line: #e2e5ee;
  --line-strong: #c9cfdd;
  --ok: #0f6b45;
  --ok-soft: #e3f2ea;
  --focus: #0a58ca;

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

  /* shape & motion */
  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 14px;
  --dur-1: 120ms;
  --dur-2: 180ms;
  --dur-3: 240ms;
  --shadow-1: 0 1px 2px rgba(24, 32, 60, 0.05), 0 1px 3px rgba(24, 32, 60, 0.04);
  --shadow-2: 0 4px 14px rgba(24, 32, 60, 0.10);
  --shadow-3: 0 18px 48px rgba(18, 24, 52, 0.22);

  --sidebar-w: 248px;
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/*
 * Global focus ring: every keyboard-focusable element gets a visible,
 * high-contrast 2px ring. (No outline suppression anywhere on this page.)
 */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible,
[role="menuitem"]:focus-visible,
[role="tab"]:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

h1, h2, h3 { line-height: 1.25; letter-spacing: -0.012em; }
h1 { font-size: 2rem; margin: 0 0 var(--sp-3); font-weight: 700; }
h2 { font-size: 1.35rem; margin: 0 0 var(--sp-2); font-weight: 700; }
h3 { font-size: 1rem; margin: 0; font-weight: 650; }
p  { margin: 0 0 var(--sp-3); }

/* ============ skip link ============ */
.skip-link {
  position: fixed;
  top: var(--sp-2);
  left: var(--sp-2);
  z-index: 200;
  transform: translateY(-260%);
  background: var(--accent);
  color: #fff;
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--r-sm);
  font-weight: 600;
  text-decoration: none;
  box-shadow: var(--shadow-2);
  transition: transform var(--dur-1) ease;
}
.skip-link:focus { transform: translateY(0); }
.skip-link:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* ============ layout ============ */
.layout { display: flex; min-height: 100vh; }

.sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  background: var(--card);
  border-right: 1px solid var(--line);
  padding: var(--sp-5) var(--sp-4) var(--sp-5);
}
.brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 0 var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--sp-4);
  font-weight: 750;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
}
.brand-mark {
  width: 26px; height: 26px;
  border-radius: 7px;
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
  color: #fff;
  display: grid;
  place-items: center;
  font-size: 13px;
  font-weight: 800;
}
.sidebar-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 var(--sp-2) var(--sp-2);
}
.side-nav { display: block; margin: 0; }
.side-nav ul { list-style: none; margin: 0; padding: 0; }
.side-nav a {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  margin: 2px 0;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  text-decoration: none;
  font-size: 0.92rem;
  font-weight: 550;
  border: 1px solid transparent;
  transition: background var(--dur-1) ease, color var(--dur-1) ease;
}
.side-nav a:hover { background: var(--accent-soft-2); color: var(--ink); }
.side-nav a.active { background: var(--accent-soft); color: var(--accent-deep); font-weight: 650; }
.side-nav .idx {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-3);
  width: 20px;
  flex-shrink: 0;
}
.side-nav a.active .idx { color: var(--accent); }
.sidebar-foot {
  margin-top: var(--sp-5);
  padding: var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-size: 0.78rem;
  color: var(--ink-2);
  background: var(--paper);
}
.sidebar-foot code { font-family: var(--font-mono); font-size: 0.72rem; color: var(--accent-deep); }

.content { flex: 1; min-width: 0; }

.page-head {
  padding: var(--sp-6) var(--sp-6) var(--sp-5);
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, #fff 0%, var(--paper) 100%);
  max-width: 900px;
}
.eyebrow {
  display: inline-block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  background: var(--accent-soft);
  padding: 3px 10px;
  border-radius: 999px;
  margin-bottom: var(--sp-3);
}
.lede { font-size: 1.05rem; color: var(--ink-2); max-width: 62ch; }

main { padding: var(--sp-5) var(--sp-6) var(--sp-7); max-width: 900px; }

/* ============ component sections ============ */
.component {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
  margin-bottom: var(--sp-6);
  overflow: hidden;
}
.component-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--line);
}
.component-head h2 { margin: 0; }
.component-desc { color: var(--ink-2); font-size: 0.95rem; margin: 0; }
.stage { padding: var(--sp-5); }

/* keyboard-contract caption */
.caption {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  background: var(--paper);
  border-top: 1px solid var(--line);
  font-size: 0.82rem;
  color: var(--ink-2);
}
.caption .cap-label {
  font-size: 10.5px;
  font-weight: 750;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-right: var(--sp-1);
}
kbd {
  display: inline-block;
  min-width: 22px;
  padding: 1px 6px;
  border: 1px solid var(--line-strong);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: #fff;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink);
  text-align: center;
  box-shadow: 0 1px 0 rgba(24,32,60,0.04);
}

/* ============ shared widget bits ============ */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  font: inherit;
  font-weight: 600;
  font-size: 0.92rem;
  border-radius: var(--r-sm);
  border: 1px solid var(--line-strong);
  background: #fff;
  color: var(--ink);
  cursor: pointer;
  transition: background var(--dur-1) ease, border-color var(--dur-1) ease, transform var(--dur-1) ease;
}
.btn:hover { background: var(--accent-soft-2); border-color: var(--accent); }
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-deep); border-color: var(--accent-deep); }
.btn-danger { background: #b3261e; border-color: #b3261e; color: #fff; }
.btn-danger:hover { background: #8f1d16; border-color: #8f1d16; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 650;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  color: var(--ink-2);
  background: #fff;
}
.chip .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3); }
.chip[data-state="open"] { color: var(--accent-deep); border-color: var(--accent); background: var(--accent-soft); }
.chip[data-state="open"] .dot { background: var(--accent); }
.chip[data-state="closed"] { color: var(--ink-3); }

.status-line {
  margin-top: var(--sp-4);
  min-height: 20px;
  font-size: 0.85rem;
  color: var(--ink-2);
  padding-left: var(--sp-3);
  border-left: 3px solid var(--line-strong);
}
.status-line strong { color: var(--ink); }

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

/* page-level inert (applied to main while the modal is open) */
main[aria-disabled="true"] { pointer-events: none; }

/* ============ responsive & motion ============ */
@media (max-width: 900px) {
  .layout { flex-direction: column; }
  .sidebar {
    position: static;
    width: 100%;
    height: auto;
    border-right: 0;
    border-bottom: 1px solid var(--line);
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    padding: var(--sp-3) var(--sp-4);
    flex-wrap: wrap;
  }
  .brand { border-bottom: 0; padding: 0; margin: 0; }
  .sidebar-label, .sidebar-foot { display: none; }
  .side-nav ul { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
  .side-nav a { padding: var(--sp-1) var(--sp-3); }
  .side-nav .idx { display: none; }
  main { padding: var(--sp-4); }
  .page-head { padding: var(--sp-5) var(--sp-4) var(--sp-4); }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
/* ── css: components ── */
/* ============ 1. Modal dialog ============ */
.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: var(--sp-5);
  background: rgba(20, 26, 48, 0.45);
  opacity: 0;
  transition: opacity var(--dur-2) ease;
}
.modal-backdrop[hidden] { display: none !important; }
.modal-backdrop.is-visible { opacity: 1; }
.modal-dialog {
  width: min(520px, 100%);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
  padding: var(--sp-5);
  transform: translateY(10px) scale(0.98);
  transition: transform var(--dur-2) ease;
}
.modal-backdrop.is-visible .modal-dialog { transform: none; }
.modal-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
}
.modal-close {
  flex-shrink: 0;
  width: 34px; height: 34px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: #fff;
  color: var(--ink-2);
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
}
.modal-close:hover { background: var(--accent-soft-2); color: var(--ink); border-color: var(--accent); }
.modal-body { color: var(--ink-2); font-size: 0.95rem; }
.modal-body .note {
  margin: var(--sp-3) 0 0;
  padding: var(--sp-3);
  background: var(--accent-soft-2);
  border: 1px solid var(--accent-soft);
  border-radius: var(--r-md);
  font-size: 0.85rem;
  color: var(--ink-2);
}
.modal-body .note code { font-family: var(--font-mono); font-size: 0.8rem; color: var(--accent-deep); }
.modal-foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-2);
  margin-top: var(--sp-5);
}

/* ============ 2. Dropdown menu ============ */
.menu-wrap { position: relative; display: inline-block; }
.menu-btn { min-width: 190px; justify-content: flex-start; }
.menu-btn .chev { margin-left: auto; transition: transform var(--dur-1) ease; color: var(--ink-3); font-size: 12px; }
.menu-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }
.menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 60;
  min-width: 230px;
  padding: var(--sp-1);
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-2);
}
.menu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  font: inherit;
  font-size: 0.92rem;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.menu-item .mi-icon { width: 20px; text-align: center; color: var(--ink-3); font-size: 14px; }
.menu-item .mi-short {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-3);
}
.menu-item[aria-disabled="true"] { color: var(--ink-3); cursor: not-allowed; }
.menu-item.is-focused { background: var(--accent-soft); color: var(--accent-deep); }
.menu-item.is-focused .mi-icon { color: var(--accent); }
.menu-item.is-focused .mi-short { color: var(--accent-deep); }
.menu-sep { height: 1px; margin: var(--sp-1) var(--sp-2); background: var(--line); }

/* ============ 3. Tabs ============ */
.tabs .tablist {
  display: flex;
  gap: 2px;
  border-bottom: 2px solid var(--line);
  margin-bottom: var(--sp-4);
}
.tab {
  appearance: none;
  border: 0;
  background: none;
  font: inherit;
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--ink-3);
  padding: var(--sp-2) var(--sp-4);
  margin-bottom: -2px;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: color var(--dur-1) ease, border-color var(--dur-1) ease;
}
.tab:hover { color: var(--ink); }
.tab[aria-selected="true"] { color: var(--accent-deep); border-bottom-color: var(--accent); }
.tab .badge {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-deep);
  font-size: 11px;
  font-weight: 750;
  font-family: var(--font-mono);
}
.tab-panel { padding: var(--sp-2) 0 var(--sp-1); }
.tab-panel h3 { margin-bottom: var(--sp-2); }
.tab-panel p { font-size: 0.95rem; color: var(--ink-2); max-width: 60ch; }
.code-block {
  margin: var(--sp-3) 0;
  padding: var(--sp-3) var(--sp-4);
  background: #0f1526;
  border-radius: var(--r-md);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  line-height: 1.6;
  color: #c7d2f0;
  overflow-x: auto;
}
.code-block .tk { color: #8ab4ff; }
.code-block .st { color: #9fe8c5; }
.link-row { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.link-row a {
  color: var(--accent);
  font-weight: 600;
  font-size: 0.92rem;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.link-row a:hover { color: var(--accent-deep); }
.spec-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; margin-top: var(--sp-2); }
.spec-table th, .spec-table td { text-align: left; padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--line); }
.spec-table th { color: var(--ink-3); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em; }
.spec-table td:first-child { font-weight: 600; }
.spec-table tr:last-child td { border-bottom: 0; }
input.field {
  font: inherit;
  font-size: 0.95rem;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--ink);
  background: #fff;
  width: 100%;
  max-width: 340px;
}

/* ============ 4. Combobox ============ */
.combo-wrap { position: relative; max-width: 440px; }
.combo-wrap .combo {
  padding-right: 40px;
  border-color: var(--line-strong);
  width: 100%;
  max-width: none;
  padding: var(--sp-2) var(--sp-3);
  padding-left: 34px;
}
.combo-wrap .combo-icon {
  position: absolute;
  left: 11px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-3);
  pointer-events: none;
  font-size: 14px;
}
.combo-wrap .combo[aria-expanded="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.combo-list {
  position: absolute;
  top: calc(100% + 4px);
  left: 0; right: 0;
  z-index: 50;
  margin: 0;
  padding: var(--sp-1);
  list-style: none;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-2);
  max-height: 236px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.combo-list[hidden] { display: none !important; }
.combo-opt {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  font-size: 0.93rem;
  color: var(--ink);
}
.combo-opt .rank {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-3);
  width: 18px;
}
.combo-opt .region { margin-left: auto; font-size: 0.78rem; color: var(--ink-3); }
.combo-opt[aria-selected="true"] { background: var(--accent); color: #fff; }
.combo-opt[aria-selected="true"] .rank, .combo-opt[aria-selected="true"] .region { color: #d6e0ff; }
.combo-opt.has-mark { font-weight: 650; }
.combo-empty {
  padding: var(--sp-3);
  font-size: 0.88rem;
  color: var(--ink-3);
  text-align: center;
}

/* ============ 5. Accordion ============ */
.accordion { border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.acc-item + .acc-item { border-top: 1px solid var(--line); }
.acc-trigger {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 0;
  background: #fff;
  font: inherit;
  font-weight: 650;
  font-size: 0.97rem;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-1) ease;
}
.acc-trigger:hover { background: var(--accent-soft-2); }
.acc-trigger .acc-num {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-3);
  width: 22px;
}
.acc-trigger .acc-tick {
  margin-left: auto;
  width: 22px; height: 22px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  color: var(--ink-3);
  font-size: 12px;
  transition: transform var(--dur-2) ease, background var(--dur-2) ease, color var(--dur-2) ease, border-color var(--dur-2) ease;
}
.acc-trigger[aria-expanded="true"] .acc-tick {
  transform: rotate(45deg);
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.acc-trigger[aria-expanded="true"] { color: var(--accent-deep); }
.acc-trigger[aria-expanded="true"] .acc-num { color: var(--accent); }
.acc-panel { overflow: hidden; }
.acc-panel[hidden] { display: none !important; }
.acc-inner {
  padding: var(--sp-1) var(--sp-4) var(--sp-4) calc(var(--sp-4) + 22px + var(--sp-3));
  color: var(--ink-2);
  font-size: 0.93rem;
  max-width: 62ch;
}
.acc-inner p { margin: var(--sp-2) 0; }
.acc-inner ul { margin: var(--sp-2) 0; padding-left: var(--sp-4); }
.acc-inner li { margin: var(--sp-1) 0; }

/* ============ footer ============ */
.page-foot {
  max-width: 900px;
  margin: 0 auto;
  padding: 0 var(--sp-6) var(--sp-6);
  color: var(--ink-3);
  font-size: 0.8rem;
}
</style>
</head>
<body>
<div id="app"><a class="skip-link" href="#main-content">Skip to main content</a>
<div class="layout">
  <aside class="sidebar">
    <div class="brand"><span class="brand-mark" aria-hidden="true">C</span> Clarity&nbsp;UI</div>
    <nav class="side-nav" aria-label="Components">
      <p class="sidebar-label">Components</p>
      <ul>
        <li><a href="#modal"><span class="idx" aria-hidden="true">01</span> Modal dialog</a></li>
        <li><a href="#menu"><span class="idx" aria-hidden="true">02</span> Dropdown menu</a></li>
        <li><a href="#tabs"><span class="idx" aria-hidden="true">03</span> Tabs</a></li>
        <li><a href="#combobox"><span class="idx" aria-hidden="true">04</span> Combobox</a></li>
        <li><a href="#accordion"><span class="idx" aria-hidden="true">05</span> Accordion</a></li>
      </ul>
    </nav>
    <div class="sidebar-foot">
      Every widget follows the <a href="#main-content" style="color:var(--accent);font-weight:600;">WAI-ARIA Authoring Practices</a>. Try each one with <code>Tab</code>, <code>Shift+Tab</code> and arrow keys — no mouse needed.
    </div>
  </aside>

  <div class="content">
    <header class="page-head">
      <span class="eyebrow">Design system · v1.0</span>
      <h1>Accessible component showcase</h1>
      <p class="lede">Five production-grade widgets built with vanilla JavaScript and hand-written CSS. Correct focus management, ARIA state that never drifts from the visual, and full keyboard operability by design.</p>
    </header>

    <main id="main-content" tabindex="-1">
      <!-- 1 ─ Modal dialog -->
      <section class="component" id="modal" aria-labelledby="modal-h">
        <div class="component-head">
          <div>
            <h2 id="modal-h">Modal dialog</h2>
            <p class="component-desc">A focus-trapped, modal surface that disables the page behind it and returns focus to its trigger on close.</p>
          </div>
          <span class="chip" id="modal-chip" data-state="closed"><span class="dot" aria-hidden="true"></span><span id="modal-chip-text">Closed</span></span>
        </div>
        <div class="stage">
          <button type="button" class="btn btn-primary" id="modal-open" aria-haspopup="dialog">Open settings dialog</button>
          <p class="status-line" id="modal-status">The dialog has not been opened yet.</p>
        </div>
        <p class="caption">
          <span class="cap-label">Keyboard</span>
          <span><kbd>Tab</kbd> cycles inside</span><span>·</span>
          <span><kbd>Esc</kbd> closes, focus returns</span><span>·</span>
          <span>backdrop click closes</span><span>·</span>
          <span>page behind is inert</span>
        </p>
      </section>

      <!-- 2 ─ Dropdown menu -->
      <section class="component" id="menu" aria-labelledby="menu-h">
        <div class="component-head">
          <div>
            <h2 id="menu-h">Dropdown menu</h2>
            <p class="component-desc">A menu button whose items respond to arrow keys like the platform expects — and whose state is exposed through ARIA.</p>
          </div>
          <span class="chip" id="menu-chip" data-state="closed"><span class="dot" aria-hidden="true"></span><span id="menu-chip-text">Closed</span></span>
        </div>
        <div class="stage">
          <div class="menu-wrap">
            <button type="button" class="btn menu-btn" id="menu-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="menu-list">
              Edit document <span class="chev" aria-hidden="true">▾</span>
            </button>
            <ul class="menu" id="menu-list" role="menu" aria-labelledby="menu-btn" hidden="">
              <li><button type="button" class="menu-item" role="menuitem" id="mi-cut" data-action="Cut" data-shortcut="⌘X"><span class="mi-icon" aria-hidden="true">✂</span>Cut<span class="mi-short" aria-hidden="true">⌘X</span></button></li>
              <li><button type="button" class="menu-item" role="menuitem" id="mi-copy" data-action="Copy" data-shortcut="⌘C"><span class="mi-icon" aria-hidden="true">⧉</span>Copy<span class="mi-short" aria-hidden="true">⌘C</span></button></li>
              <li><button type="button" class="menu-item" role="menuitem" id="mi-paste" data-action="Paste" data-shortcut="⌘V"><span class="mi-icon" aria-hidden="true">⎘</span>Paste<span class="mi-short" aria-hidden="true">⌘V</span></button></li>
              <li aria-hidden="true"><div class="menu-sep"></div></li>
              <li><button type="button" class="menu-item" role="menuitem" id="mi-rename" data-action="Rename"><span class="mi-icon" aria-hidden="true">✎</span>Rename<span class="mi-short" aria-hidden="true"></span></button></li>
              <li><button type="button" class="menu-item" role="menuitem" id="mi-share" data-action="Share link"><span class="mi-icon" aria-hidden="true">↗</span>Share link<span class="mi-short" aria-hidden="true"></span></button></li>
              <li><button type="button" class="menu-item" role="menuitem" id="mi-duplicate" data-action="Duplicate" aria-disabled="true"><span class="mi-icon" aria-hidden="true">⊞</span>Duplicate<span class="mi-short" aria-hidden="true">soon</span></button></li>
            </ul>
          </div>
          <p class="status-line" id="menu-status">No action performed yet.</p>
        </div>
        <p class="caption">
          <span class="cap-label">Keyboard</span>
          <span><kbd>↓</kbd> opens on first item</span><span>·</span>
          <span><kbd>↑</kbd><kbd>↓</kbd> wrap around</span><span>·</span>
          <span><kbd>Home</kbd>/<kbd>End</kbd> jump</span><span>·</span>
          <span><kbd>Esc</kbd> or <kbd>Tab</kbd> close</span>
        </p>
      </section>

      <!-- 3 ─ Tabs -->
      <section class="component" id="tabs" aria-labelledby="tabs-h">
        <div class="component-head">
          <div>
            <h2 id="tabs-h">Tabs</h2>
            <p class="component-desc">Roving tabindex with <code>aria-selected</code>, <code>aria-controls</code> and <code>aria-labelledby</code> wired end-to-end. Hidden panels are truly removed from the a11y tree.</p>
          </div>
        </div>
        <div class="stage">
          <div class="tabs">
            <div class="tablist" role="tablist" aria-label="Component documentation" id="tablist">
              <button type="button" class="tab" role="tab" id="tab-1" aria-controls="panel-1" aria-selected="true" tabindex="0">Overview</button>
              <button type="button" class="tab" role="tab" id="tab-2" aria-controls="panel-2" aria-selected="false" tabindex="-1">Usage<span class="badge" aria-hidden="true">4</span></button>
              <button type="button" class="tab" role="tab" id="tab-3" aria-controls="panel-3" aria-selected="false" tabindex="-1">Accessibility</button>
            </div>
            <div class="tab-panel" role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">
              <h3>What this component does</h3>
              <p>Tabs let users switch between large related views without leaving the page. The selected tab is always reachable with a single <kbd>Tab</kbd> press, and arrow keys move between tabs without hunting for focus.</p>
              <div class="link-row">
                <a href="#tabs">Anchor link inside the panel</a>
                <a href="#modal">Jump to the modal section</a>
              </div>
            </div>
            <div class="tab-panel" role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0" hidden="">
              <h3>Dropping it into your code</h3>
              <pre class="code-block" tabindex="0" aria-label="Code sample"><span class="tk">import</span> { Tabs } <span class="tk">from</span> <span class="st">'@clarity/tabs'</span>;

Tabs.mount(document.querySelector(<span class="st">'#docs-tabs'</span>), {
  default: <span class="st">'overview'</span>,
  animated: <span class="st">true</span>
});</pre>
              <p>The <code>Usage</code> tab carries a badge with a count — a visual affordance that never affects the tab's accessible name.</p>
            </div>
            <div class="tab-panel" role="tabpanel" id="panel-3" aria-labelledby="tab-3" tabindex="0" hidden="">
              <h3>Why these patterns</h3>
              <table class="spec-table">
                <thead><tr><th>Property</th><th>Value</th><th>Meaning</th></tr></thead>
                <tbody>
                  <tr><td>tabindex</td><td>0 on selected only</td><td>one stop in the tab order</td></tr>
                  <tr><td>aria-selected</td><td>true / false</td><td>announcement on selection</td></tr>
                  <tr><td>hidden</td><td>on inactive panels</td><td>content leaves the a11y tree</td></tr>
                  <tr><td>← / →</td><td>roves focus</td><td>with wrap-around</td></tr>
                </tbody>
              </table>
              <p class="link-row" style="margin-top:16px;"><a href="#combobox">Compare with the combobox pattern</a></p>
            </div>
          </div>
        </div>
        <p class="caption">
          <span class="cap-label">Keyboard</span>
          <span><kbd>←</kbd><kbd>→</kbd> move focus &amp; select</span><span>·</span>
          <span><kbd>Home</kbd>/<kbd>End</kbd> first/last</span><span>·</span>
          <span><kbd>Tab</kbd> enters panel content</span>
        </p>
      </section>

      <!-- 4 ─ Combobox -->
      <section class="component" id="combobox" aria-labelledby="combo-h">
        <div class="component-head">
          <div>
            <h2 id="combo-h">Combobox</h2>
            <p class="component-desc">An autocomplete input using the <code>aria-activedescendant</code> pattern: the listbox is never focusable, and the highlighted option is tracked on the input itself.</p>
          </div>
          <span class="chip" id="combo-chip" data-state="closed"><span class="dot" aria-hidden="true"></span><span id="combo-chip-text">Idle</span></span>
        </div>
        <div class="stage">
          <div class="combo-wrap">
            <span class="combo-icon" aria-hidden="true">⌕</span>
            <input type="text" class="field combo" id="combo-input" role="combobox" aria-expanded="false" aria-controls="combo-listbox" aria-autocomplete="list" aria-labelledby="combo-label combo-input" placeholder="Search for a city…" autocomplete="off" spellcheck="false">
            <ul class="combo-list" id="combo-listbox" role="listbox" aria-labelledby="combo-label" hidden=""></ul>
          </div>
          <label class="sr-only" id="combo-label" for="combo-input">City</label>
          <p class="status-line" id="combo-status">Start typing to search 20 cities. Arrow keys highlight, <strong>Enter</strong> selects.</p>
        </div>
        <p class="caption">
          <span class="cap-label">Keyboard</span>
          <span>type to filter</span><span>·</span>
          <span><kbd>↓</kbd><kbd>↑</kbd> move highlight</span><span>·</span>
          <span><kbd>Enter</kbd> selects</span><span>·</span>
          <span><kbd>Esc</kbd> closes &amp; reverts</span>
        </p>
      </section>

      <!-- 5 ─ Accordion -->
      <section class="component" id="accordion" aria-labelledby="accordion-h">
        <div class="component-head">
          <div>
            <h2 id="accordion-h">Accordion</h2>
            <p class="component-desc">A disclosure group where any number of regions may be open at once. Height animates first; <code>hidden</code> is only touched at the end of the transition.</p>
          </div>
        </div>
        <div class="stage">
          <div class="accordion" role="group" aria-labelledby="accordion-h">
            <div class="acc-item">
              <h3><button type="button" class="acc-trigger" id="acc-t1" aria-expanded="true" aria-controls="acc-p1">Installation<span class="acc-num" aria-hidden="true">01</span><span class="acc-tick" aria-hidden="true">+</span></button></h3>
              <div class="acc-panel" id="acc-p1" role="region" aria-labelledby="acc-t1">
                <div class="acc-inner">
                  <p>Install from the registry and add the stylesheet once:</p>
                  <pre class="code-block" aria-label="Install command"><span class="tk">npm</span> install @clarity/ui @clarity/styles</pre>
                  <p>Clarity UI ships per-component entry points, so only the code you use ends up in your bundle.</p>
                </div>
              </div>
            </div>
            <div class="acc-item">
              <h3><button type="button" class="acc-trigger" id="acc-t2" aria-expanded="false" aria-controls="acc-p2">Theming &amp; tokens<span class="acc-num" aria-hidden="true">02</span><span class="acc-tick" aria-hidden="true">+</span></button></h3>
              <div class="acc-panel" id="acc-p2" role="region" aria-labelledby="acc-t2" hidden="">
                <div class="acc-inner">
                  <p>Every color, radius and duration is a CSS custom property, documented in one constants block:</p>
                  <ul>
                    <li><code>--accent</code> · <code>--ink</code> · <code>--paper</code> — the palette</li>
                    <li><code>--sp-1</code> … <code>--sp-7</code> — the spacing scale</li>
                    <li><code>--dur-1…3</code> · <code>--r-sm…lg</code> — motion and shape</li>
                  </ul>
                  <p>Override any token on <code>:root</code> and the whole system re-skins.</p>
                </div>
              </div>
            </div>
            <div class="acc-item">
              <h3><button type="button" class="acc-trigger" id="acc-t3" aria-expanded="false" aria-controls="acc-p3">Accessibility checklist<span class="acc-num" aria-hidden="true">03</span><span class="acc-tick" aria-hidden="true">+</span></button></h3>
              <div class="acc-panel" id="acc-p3" role="region" aria-labelledby="acc-t3" hidden="">
                <div class="acc-inner">
                  <ul>
                    <li>Landmarks: one <code>main</code>, labelled <code>nav</code> regions</li>
                    <li>Headings: a single <code>h1</code>, one <code>h2</code> per section</li>
                    <li>Focus: visible 2px ring everywhere, never removed</li>
                    <li>State: <code>aria-expanded</code> / <code>aria-selected</code> always mirror the UI</li>
                    <li>Motion: honours <code>prefers-reduced-motion</code></li>
                  </ul>
                </div>
              </div>
            </div>
          </div>
        </div>
        <p class="caption">
          <span class="cap-label">Keyboard</span>
          <span><kbd>Tab</kbd> reaches every trigger</span><span>·</span>
          <span><kbd>Enter</kbd>/<kbd>Space</kbd> toggles</span><span>·</span>
          <span>multiple regions may be open</span>
        </p>
      </section>
    </main>

    <footer class="page-foot">
      <p>Clarity UI — a fictional design system built for accessibility demonstration. Vanilla TS, zero dependencies. View source: every interaction is a few lines of readable JavaScript.</p>
    </footer>
  </div>
</div><!-- Modal dialog (outside the layout so inert() can cover the whole page) -->
<div class="modal-backdrop" id="modal-backdrop" hidden="">
  <div class="modal-dialog" id="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-desc">
    <div class="modal-head">
      <h3 id="modal-title">Settings</h3>
      <button type="button" class="modal-close" id="modal-close" aria-label="Close dialog">✕</button>
    </div>
    <div class="modal-body">
      <p id="modal-desc">This dialog is modal: the page behind is inert, and Tab can not leave. Close it with Esc, the ✕ button, or by clicking the backdrop — focus will return to the trigger.</p>
      <label for="modal-theme">Accent intensity</label>
      <select id="modal-theme" class="field" style="margin-top:6px;">
        <option>Subtle</option>
        <option>Balanced</option>
        <option>High contrast</option>
      </select>
      <p class="note">The focus trap is a tiny shared utility (<code>trapFocus</code>) — Escape and Tab/Shift+Tab are handled in one place.</p>
    </div>
    <div class="modal-foot">
      <button type="button" class="btn" id="modal-cancel">Cancel</button>
      <button type="button" class="btn btn-primary" id="modal-confirm">Save settings</button>
    </div>
  </div>
</div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./a11y":"src/a11y.ts","./components/modal":"src/components/modal.ts","./components/menu":"src/components/menu.ts","./components/tabs":"src/components/tabs.ts","./components/combobox":"src/components/combobox.ts","./components/accordion":"src/components/accordion.ts"},"src/a11y.ts":{"./dom":"src/dom.ts"},"src/components/modal.ts":{"../dom":"src/dom.ts","../a11y":"src/a11y.ts","../config":"src/config.ts"},"src/components/menu.ts":{"../dom":"src/dom.ts","../a11y":"src/a11y.ts","../config":"src/config.ts"},"src/components/tabs.ts":{"../dom":"src/dom.ts","../a11y":"src/a11y.ts"},"src/components/combobox.ts":{"../dom":"src/dom.ts","../a11y":"src/a11y.ts","../config":"src/config.ts"},"src/components/accordion.ts":{"../dom":"src/dom.ts","../a11y":"src/a11y.ts","../config":"src/config.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
/**
 * ============ Entry ============
 * Runs after the DOM is parsed. Wires tokens → CSS, initializes the
 * five components, and sets up the sidebar scrollspy.
 */
const config_1 = require("./config");
const a11y_1 = require("./a11y");
const modal_1 = require("./components/modal");
const menu_1 = require("./components/menu");
const tabs_1 = require("./components/tabs");
const combobox_1 = require("./components/combobox");
const accordion_1 = require("./components/accordion");
/** Push tunable constants onto CSS custom properties (single source of truth). */
function applyTokens() {
    const r = document.documentElement;
    r.style.setProperty('--accent', config_1.CONFIG.colors.accent);
    r.style.setProperty('--accent-deep', config_1.CONFIG.colors.accentDeep);
    config_1.CONFIG.spacing.forEach((px, i) => r.style.setProperty(`--sp-${i + 1}`, `${px}px`));
    r.style.setProperty('--dur-acc', `${config_1.CONFIG.accordionMs}ms`);
}
/** Sidebar link active state following the section in view. */
function scrollSpy() {
    const links = Array.from(document.querySelectorAll('.side-nav a'));
    const byId = new Map(links.map((a) => [a.getAttribute('href')?.slice(1), a]));
    const sections = Array.from(document.querySelectorAll('section.component'));
    const io = new IntersectionObserver((entries) => {
        for (const e of entries) {
            if (e.isIntersecting) {
                for (const l of links) {
                    l.classList.remove('active');
                    l.removeAttribute('aria-current');
                }
                const cur = byId.get(e.target.id);
                cur?.classList.add('active');
                cur?.setAttribute('aria-current', 'true');
            }
        }
    }, { rootMargin: '-20% 0px -65% 0px' });
    sections.forEach((s) => io.observe(s));
}
function start() {
    document.documentElement.lang = 'en';
    applyTokens();
    (0, a11y_1.syncAriaState)(true);
    (0, modal_1.modalInit)();
    (0, menu_1.menuInit)();
    (0, tabs_1.tabsInit)();
    (0, combobox_1.comboboxInit)();
    (0, accordion_1.accordionInit)();
    scrollSpy();
}
start();
};

// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
/**
 * ============ Tunable constants ============
 * Edit these to re-theme / re-time the whole showcase.
 */
exports.CONFIG = {
    colors: {
        accent: '#3653d9',
        accentDeep: '#26379e',
        ink: '#1c2433',
        paper: '#f5f6fa',
    },
    /** spacing scale in px (1 → 7) */
    spacing: [4, 8, 12, 16, 24, 32, 48],
    /** animation durations in ms */
    durations: { fast: 120, base: 180, slow: 240 },
    /** accordion height-animation duration (also set as --dur-acc) */
    accordionMs: 220,
    /** menu: initial index for Down-arrow open */
    menu: { initialIndex: 0 },
    /** combobox: max results shown in the listbox */
    combobox: { maxResults: 20 },
};
};

// ── module: src/dom.ts ──
__mods["src/dom.ts"] = function (exports, require, module) {
"use strict";
/**
 * ============ Tiny DOM helpers ============
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.$ = $;
exports.$all = $all;
exports.el = el;
exports.isFocusable = isFocusable;
exports.focusableIn = focusableIn;
exports.setAttr = setAttr;
function $(sel, root = document) {
    return root.querySelector(sel);
}
function $all(sel, root = document) {
    return Array.from(root.querySelectorAll(sel));
}
/** Create an element of `tag` with attributes and (optional) text content. */
function el(tag, attrs = {}, text = '') {
    const node = document.createElement(tag);
    for (const [k, v] of Object.entries(attrs))
        node.setAttribute(k, String(v));
    if (text)
        node.textContent = text;
    return node;
}
function isFocusable(node) {
    if ('disabled' in node && node.disabled)
        return false;
    const tag = node.tagName.toLowerCase();
    if (tag === 'script' || tag === 'style' || tag === 'link' || tag === 'meta')
        return false;
    if (node.hasAttribute('tabindex') && !node.hasAttribute('hidden')) {
        const ti = node.getAttribute('tabindex') ?? '0';
        if (ti !== '' && !isNaN(Number(ti)))
            return true;
    }
    const interactive = ['a', 'button', 'input', 'select', 'textarea'];
    if (interactive.includes(tag)) {
        if (tag === 'a' && !node.hasAttribute('href'))
            return false;
        if (tag === 'input' && node.getAttribute('type') === 'hidden')
            return false;
        return true;
    }
    return false;
}
/** All currently focusable elements under `root`, in DOM (tab) order. */
function focusableIn(root) {
    return $all('*', root).filter((n) => !n.closest('[hidden]') && isFocusable(n));
}
function setAttr(node, key, value) {
    if (!node)
        return;
    if (value === null)
        node.removeAttribute(key);
    else
        node.setAttribute(key, value);
}
};

// ── module: src/a11y.ts ──
__mods["src/a11y.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.announce = announce;
exports.syncAriaState = syncAriaState;
exports.trapFocus = trapFocus;
exports.roving = roving;
/**
 * ============ Shared a11y primitives ============
 * Live region, focus-trap manager, and a generic roving
 * highlight manager (aria-activedescendant pattern) for
 * menu-style widgets.
 */
const dom_1 = require("./dom");
/* ---------- live region ---------- */
let live = null;
/** Announce `msg` politely to assistive tech. */
function announce(msg) {
    if (!live) {
        live = (0, dom_1.el)('div', {
            class: 'sr-only',
            id: 'a11y-live',
            role: 'status',
            'aria-live': 'polite',
            'aria-atomic': 'true',
        });
        document.body.appendChild(live);
    }
    live.textContent = '';
    // Two frames so AT picks up even an identical re-announcement.
    requestAnimationFrame(() => {
        if (live)
            live.textContent = msg;
    });
}
/* ---------- global state sync ---------- */
/**
 * Keep `aria-expanded` (menu/accordion/combobox triggers) and
 * `aria-selected` (tabs) in lock-step with the DOM: the source of truth
 * is always the presence/absence of the `hidden` attribute on the
 * controlled element (or on the button itself, for tabs). Call after any
 * mutation, and with `force` when a global event (e.g. the modal opening)
 * may have changed visibility while components were asleep.
 */
function syncAriaState(force = false) {
    if (force) {
        for (const b of (0, dom_1.$all)('button[data-sync]'))
            delete b.dataset.sync;
    }
    for (const b of (0, dom_1.$all)('button[aria-expanded]')) {
        const target = document.getElementById(b.getAttribute('aria-controls') || '');
        if (!target)
            continue;
        const open = !target.hasAttribute('hidden');
        if (b.dataset.sync !== String(open)) {
            b.setAttribute('aria-expanded', String(open));
            b.dataset.sync = String(open);
        }
    }
    for (const b of (0, dom_1.$all)('button[aria-selected]')) {
        const target = document.getElementById(b.getAttribute('aria-controls') || '');
        // tabs derive selected-state from their panel's hidden attribute
        const selected = target ? !target.hasAttribute('hidden') : !b.hasAttribute('hidden');
        if (b.dataset.sync !== String(selected)) {
            b.setAttribute('aria-selected', String(selected));
            b.dataset.sync = String(selected);
        }
    }
}
/**
 * Make `blockers` inert while `dialog` is open:
 * - `inert` property (modern browsers) + `aria-disabled` (fallback)
 * - Escape closes via `onEscape`
 * - Tab / Shift+Tab cycle inside the dialog
 * - focus moves to the first focusable in the dialog
 */
function trapFocus(dialog, blockers, onEscape) {
    const restore = [];
    for (const r of blockers) {
        restore.push([r, r.getAttribute('aria-disabled')]);
        r.setAttribute('aria-disabled', 'true');
        try {
            r.inert = true;
        }
        catch {
            /* attribute-only fallback */
        }
    }
    const keyHandler = (e) => {
        if (e.key === 'Escape') {
            e.stopPropagation();
            onEscape();
            return;
        }
        if (e.key !== 'Tab')
            return;
        const items = (0, dom_1.focusableIn)(dialog);
        if (items.length === 0) {
            e.preventDefault();
            return;
        }
        const active = document.activeElement;
        if (!active || !dialog.contains(active)) {
            // focus wandered out (or never entered) — pull it back in
            e.preventDefault();
            items[0].focus();
            return;
        }
        const i = items.indexOf(active);
        e.preventDefault(); // always move focus manually: deterministic + wraps
        if (e.shiftKey) {
            items[i <= 0 ? items.length - 1 : i - 1].focus();
        }
        else {
            items[i >= items.length - 1 ? 0 : i + 1].focus();
        }
    };
    document.addEventListener('keydown', keyHandler);
    const target = (0, dom_1.focusableIn)(dialog)[0] ?? dialog;
    target.focus();
    return {
        release() {
            document.removeEventListener('keydown', keyHandler);
            for (const [node, prev] of restore) {
                if (prev === null)
                    node.removeAttribute('aria-disabled');
                else
                    node.setAttribute('aria-disabled', prev);
                try {
                    node.inert = false;
                }
                catch {
                    /* ignore */
                }
            }
        },
    };
}
/**
 * Roving highlight manager for the aria-activedescendant pattern:
 * the controlled list itself is never focusable; the *input/button*
 * keeps focus while this manager moves the highlight. Hidden or
 * aria-disabled items are skipped; navigation wraps by default.
 */
function roving(opts) {
    const wrap = opts.wrap ?? true;
    let idx = -1;
    const items = () => opts
        .getItems()
        .filter((i) => !i.hasAttribute('hidden') && i.getAttribute('aria-disabled') !== 'true');
    function highlight(i) {
        const list = items();
        if (list.length === 0)
            return;
        idx = Math.max(0, Math.min(i, list.length - 1));
        list.forEach((it, n) => {
            const on = n === idx;
            it.classList.toggle('is-focused', on);
            if (it.hasAttribute('aria-selected'))
                it.setAttribute('aria-selected', String(on));
        });
        opts.onMove?.(idx, list[idx]);
    }
    return {
        items,
        index: () => idx,
        current: () => {
            const list = items();
            return idx >= 0 && idx < list.length ? list[idx] : null;
        },
        highlight,
        move(dir) {
            const list = items();
            if (list.length === 0)
                return;
            if (idx < 0)
                highlight(dir > 0 ? 0 : list.length - 1);
            else if (wrap)
                highlight((idx + dir + list.length) % list.length);
            else
                highlight(idx + dir);
        },
        first: () => highlight(0),
        last: () => highlight(items().length - 1),
        clear() {
            items().forEach((it) => {
                it.classList.remove('is-focused');
                if (it.hasAttribute('aria-selected'))
                    it.setAttribute('aria-selected', 'false');
            });
            idx = -1;
        },
    };
}
};

// ── module: src/components/modal.ts ──
__mods["src/components/modal.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.modalInit = modalInit;
/**
 * ============ 1. Modal dialog ============
 * role="dialog" aria-modal="true", focus-trapped, inert background,
 * focus returns to the trigger on close.
 */
const dom_1 = require("../dom");
const a11y_1 = require("../a11y");
const config_1 = require("../config");
const openBtn = (0, dom_1.$)('#modal-open');
const statusEl = (0, dom_1.$)('#modal-status');
const chip = (0, dom_1.$)('#modal-chip');
const chipText = (0, dom_1.$)('#modal-chip-text');
const backdrop = (0, dom_1.$)('#modal-backdrop');
const dialog = (0, dom_1.$)('#modal-dialog');
const closeBtn = (0, dom_1.$)('#modal-close');
const confirmBtn = (0, dom_1.$)('#modal-confirm');
const cancelBtn = (0, dom_1.$)('#modal-cancel');
const themeSelect = (0, dom_1.$)('#modal-theme');
let trap = null;
let open = false;
let closeTimer = 0;
let hideOnEnd = null;
function setChip(state) {
    chip.dataset.state = state;
    chipText.textContent = state === 'open' ? 'Open' : 'Closed';
}
function openDialog() {
    if (open)
        return;
    open = true;
    window.clearTimeout(closeTimer); // cancel a pending fade-out
    if (hideOnEnd) {
        backdrop.removeEventListener('transitionend', hideOnEnd);
        hideOnEnd = null;
    }
    backdrop.hidden = false;
    // let the browser commit the DOM, then fade in + trap focus
    requestAnimationFrame(() => {
        backdrop.classList.add('is-visible');
        trap = (0, a11y_1.trapFocus)(dialog, [
            (0, dom_1.$)('.sidebar'),
            (0, dom_1.$)('.page-head'),
            (0, dom_1.$)('#main-content'),
        ], () => closeDialog());
        (0, a11y_1.announce)('Settings dialog opened. Focus is trapped inside the dialog.');
    });
    setChip('open');
    statusEl.innerHTML = 'Dialog is open — <strong>Tab cycles inside</strong>, <strong>Esc</strong> closes and returns focus here.';
}
function closeDialog() {
    if (!open)
        return;
    open = false;
    backdrop.classList.remove('is-visible');
    trap?.release();
    trap = null;
    hideOnEnd = () => {
        backdrop.hidden = true;
        backdrop.removeEventListener('transitionend', hideOnEnd);
        hideOnEnd = null;
    };
    backdrop.addEventListener('transitionend', hideOnEnd); // instant under reduced motion
    closeTimer = window.setTimeout(hideOnEnd, config_1.CONFIG.durations.base + 40); // safety net
    openBtn.focus(); // focus returns to the trigger
    setChip('closed');
    statusEl.innerHTML = `Closed — focus returned to <strong>“${openBtn.textContent}”</strong>. Theme choice: ${themeSelect.options[themeSelect.selectedIndex].text}.`;
    (0, a11y_1.announce)('Dialog closed. Focus returned to the open button.');
}
openBtn.addEventListener('click', openDialog);
closeBtn.addEventListener('click', closeDialog);
cancelBtn.addEventListener('click', closeDialog);
confirmBtn.addEventListener('click', closeDialog);
// Clicking the backdrop itself (not the dialog) closes.
backdrop.addEventListener('mousedown', (e) => {
    if (e.target === backdrop)
        closeDialog();
});
function modalInit() {
    /* dialog markup lives in the static DOM; nothing else to do */
}
};

// ── module: src/components/menu.ts ──
__mods["src/components/menu.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.menuInit = menuInit;
/**
 * ============ 2. Dropdown menu ============
 * WAI "menu button" pattern:
 *  - button: aria-haspopup="menu", aria-expanded
 *  - on open, focus moves into the menu (first or last item, depending
 *    on how it was opened); items use a roving tabindex (0 on the
 *    current one only, so the menu adds one tab stop)
 *  - Up/Down wrap, Home/End jump
 *  - Escape closes and returns focus to the button
 *  - Tab closes and moves focus naturally onward (Shift+Tab → button)
 *  - Enter/Space on an item selects it: visible action + close + focus
 *    back on the button
 */
const dom_1 = require("../dom");
const a11y_1 = require("../a11y");
const config_1 = require("../config");
const btn = (0, dom_1.$)('#menu-btn');
const menu = (0, dom_1.$)('#menu-list');
const statusEl = (0, dom_1.$)('#menu-status');
const chip = (0, dom_1.$)('#menu-chip');
const chipText = (0, dom_1.$)('#menu-chip-text');
let items = []; // selectable items, in order
function setChip(open) {
    chip.dataset.state = open ? 'open' : 'closed';
    chipText.textContent = open ? 'Open' : 'Closed';
}
function mark(item) {
    for (const it of items)
        it.classList.toggle('is-focused', it === item);
}
function enabledItems() {
    return items.filter((i) => i.getAttribute('aria-disabled') !== 'true');
}
function openMenu(toLast) {
    if (!menu.hidden)
        return;
    menu.hidden = false;
    btn.setAttribute('aria-expanded', 'true');
    const list = enabledItems();
    const target = list.length ? (toLast ? list[list.length - 1] : list[0]) : null;
    if (target) {
        for (const it of items)
            it.tabIndex = it === target ? 0 : -1;
        target.focus();
    }
    mark(target);
    setChip(true);
}
function closeMenu(refocus) {
    if (menu.hidden)
        return;
    menu.hidden = true;
    btn.setAttribute('aria-expanded', 'false');
    mark(null);
    setChip(false);
    if (refocus === 'button') {
        btn.focus();
    }
    else if (refocus === 'next') {
        // natural forward motion: the first focusable element after the button
        const all = (0, dom_1.focusableIn)(document.body);
        const i = all.indexOf(btn);
        const next = i >= 0 ? all[i + 1] : null;
        (next ?? btn).focus();
    }
}
function select(item) {
    const label = item.getAttribute('data-action') || '';
    closeMenu(null);
    btn.focus();
    statusEl.innerHTML = `Action performed: <strong>${label}</strong> — menu closed, focus back on the button.`;
    (0, a11y_1.announce)(`${label} selected`);
}
/* ---------- button keys ---------- */
btn.addEventListener('click', () => {
    if (menu.hidden)
        openMenu(false);
    else
        closeMenu(null);
});
btn.addEventListener('keydown', (e) => {
    const k = e.key;
    if (k === 'ArrowDown' || k === ' ' || k === 'Enter') {
        e.preventDefault(); // avoid the implicit click() double-firing
        openMenu(false);
    }
    else if (k === 'ArrowUp') {
        e.preventDefault();
        openMenu(true);
    }
    else if (k === 'Escape' && !menu.hidden) {
        e.preventDefault();
        closeMenu('button');
    }
});
/* ---------- menu (item) keys: focus lives on the items ---------- */
menu.addEventListener('keydown', (e) => {
    const k = e.key;
    const current = document.activeElement;
    const list = enabledItems();
    const idx = current ? list.indexOf(current) : -1;
    const goto = (i) => {
        const n = list.length;
        const next = list[(i + n) % n];
        for (const it of items)
            it.tabIndex = it === next ? 0 : -1;
        next.focus();
        mark(next);
    };
    if (k === 'ArrowDown') {
        e.preventDefault();
        goto(idx + 1);
    }
    else if (k === 'ArrowUp') {
        e.preventDefault();
        goto(idx - 1);
    }
    else if (k === 'Home') {
        e.preventDefault();
        goto(0);
    }
    else if (k === 'End') {
        e.preventDefault();
        goto(list.length - 1);
    }
    else if (k === 'Enter' || k === ' ') {
        e.preventDefault();
        if (current && items.includes(current)) {
            if (current.getAttribute('aria-disabled') !== 'true') {
                select(current);
            }
        }
    }
    else if (k === 'Escape') {
        e.preventDefault();
        closeMenu('button');
    }
    else if (k === 'Tab') {
        e.preventDefault();
        closeMenu(e.shiftKey ? 'button' : 'next');
    }
});
/* ---------- pointer ---------- */
menu.addEventListener('click', (e) => {
    const item = e.target.closest('[role="menuitem"]');
    if (item && item.getAttribute('aria-disabled') !== 'true')
        select(item);
});
document.addEventListener('click', (e) => {
    if (!menu.hidden && !menu.contains(e.target) && e.target !== btn) {
        closeMenu(null);
    }
});
function menuInit() {
    items = (0, dom_1.$all)('[role="menuitem"]', menu);
    for (const it of items)
        it.tabIndex = -1; // closed menu: no tab stops
    void config_1.CONFIG; // (initial index is first, per APG)
}
};

// ── module: src/components/tabs.ts ──
__mods["src/components/tabs.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tabsInit = tabsInit;
/**
 * ============ 3. Tabs ============
 * Roving tabindex: the active tab is the only one in the tab order.
 * Left/Right (Home/End) move focus AND select. Inactive panels carry
 * the `hidden` attribute, so they are never focusable.
 */
const dom_1 = require("../dom");
const a11y_1 = require("../a11y");
const tabs = (0, dom_1.$all)('[role="tab"]');
const panels = (0, dom_1.$all)('[role="tabpanel"]');
function selectTab(tab, focus) {
    for (const t of tabs) {
        const on = t === tab;
        t.setAttribute('aria-selected', String(on));
        t.tabIndex = on ? 0 : -1;
        const panel = document.getElementById(t.getAttribute('aria-controls') || '');
        if (panel) {
            if (on)
                panel.removeAttribute('hidden');
            else
                panel.setAttribute('hidden', '');
        }
    }
    if (focus)
        tab.focus();
    (0, a11y_1.announce)(`${tab.textContent?.trim()} tab selected`);
}
for (const tab of tabs) {
    tab.addEventListener('click', () => selectTab(tab, false));
    tab.addEventListener('keydown', (e) => {
        const k = e.key;
        const idx = tabs.indexOf(tab);
        let next = null;
        if (k === 'ArrowRight')
            next = tabs[(idx + 1) % tabs.length];
        else if (k === 'ArrowLeft')
            next = tabs[(idx - 1 + tabs.length) % tabs.length];
        else if (k === 'Home')
            next = tabs[0];
        else if (k === 'End')
            next = tabs[tabs.length - 1];
        if (next) {
            e.preventDefault();
            selectTab(next, true);
        }
    });
}
function tabsInit() {
    // ensure consistent initial state (tab 1 active, others hidden)
    const first = tabs[0];
    if (first)
        selectTab(first, false);
}
};

// ── module: src/components/combobox.ts ──
__mods["src/components/combobox.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.comboboxInit = comboboxInit;
/**
 * ============ 4. Combobox (autocomplete) ============
 * aria-activedescendant pattern: the input keeps focus; the listbox
 * is display-managed, never focusable. Typing filters ~20 options,
 * arrows move the highlight, Enter selects, Escape closes and reverts,
 * blur closes.
 */
const dom_1 = require("../dom");
const a11y_1 = require("../a11y");
const config_1 = require("../config");
const CITIES = [
    { name: 'Amsterdam', country: 'Netherlands', region: 'EU' },
    { name: 'Athens', country: 'Greece', region: 'EU' },
    { name: 'Barcelona', country: 'Spain', region: 'EU' },
    { name: 'Berlin', country: 'Germany', region: 'EU' },
    { name: 'Bogotá', country: 'Colombia', region: 'SA' },
    { name: 'Boston', country: 'USA', region: 'NA' },
    { name: 'Budapest', country: 'Hungary', region: 'EU' },
    { name: 'Cairo', country: 'Egypt', region: 'AF' },
    { name: 'Cape Town', country: 'South Africa', region: 'AF' },
    { name: 'Chicago', country: 'USA', region: 'NA' },
    { name: 'Copenhagen', country: 'Denmark', region: 'EU' },
    { name: 'Prague', country: 'Czechia', region: 'EU' },
    { name: 'Helsinki', country: 'Finland', region: 'EU' },
    { name: 'Kyoto', country: 'Japan', region: 'AP' },
    { name: 'Lisbon', country: 'Portugal', region: 'EU' },
    { name: 'Lyon', country: 'France', region: 'EU' },
    { name: 'Madrid', country: 'Spain', region: 'EU' },
    { name: 'Melbourne', country: 'Australia', region: 'AP' },
    { name: 'Mexico City', country: 'Mexico', region: 'NA' },
    { name: 'Mumbai', country: 'India', region: 'AP' },
];
const input = (0, dom_1.$)('#combo-input');
const listbox = (0, dom_1.$)('#combo-listbox');
const statusEl = (0, dom_1.$)('#combo-status');
const chip = (0, dom_1.$)('#combo-chip');
const chipText = (0, dom_1.$)('#combo-chip-text');
let lastValue = '';
let listOpen = false;
let options = [];
const mgr = (0, a11y_1.roving)({
    getItems: () => Array.from(listbox.querySelectorAll('[role="option"]')),
    wrap: false,
    onMove: (_i, item) => {
        input.setAttribute('aria-activedescendant', item.id);
        item.scrollIntoView({ block: 'nearest' });
    },
});
function setChip(state) {
    chip.dataset.state = state;
    chipText.textContent = state === 'open' ? 'Open' : 'Idle';
}
function renderList() {
    listbox.innerHTML = '';
    if (options.length === 0) {
        const empty = (0, dom_1.el)('li', { class: 'combo-empty', 'aria-disabled': 'true' });
        empty.textContent = 'No matches — try another prefix.';
        listbox.appendChild(empty);
        return;
    }
    options.forEach((c, i) => {
        const li = (0, dom_1.el)('li', { role: 'option', id: `combo-opt-${i}`, 'data-value': c.name, 'aria-selected': 'false' });
        const rank = (0, dom_1.el)('span', { class: 'rank', 'aria-hidden': 'true' }, String(i + 1));
        const label = (0, dom_1.el)('span', { class: 'opt-name' }, c.name);
        const region = (0, dom_1.el)('span', { class: 'region', 'aria-hidden': 'true' }, c.region);
        li.append(rank, label, region);
        listbox.appendChild(li);
    });
}
function openList(highlightFirst) {
    renderList();
    listbox.hidden = false;
    input.setAttribute('aria-expanded', 'true');
    if (highlightFirst && options.length > 0) {
        mgr.highlight(0);
    }
    else {
        input.setAttribute('aria-activedescendant', '');
    }
    setChip('open');
}
function closeList() {
    if (!listOpen)
        return;
    listOpen = false;
    listbox.hidden = true;
    input.setAttribute('aria-expanded', 'false');
    input.setAttribute('aria-activedescendant', '');
    mgr.clear();
    setChip('closed');
    // The value when the list closes becomes the revert target for the
    // next Escape (e.g. the just-selected option survives a closed-loop edit).
    lastValue = input.value;
}
function selectOption(item) {
    if (!item || !item.getAttribute('data-value'))
        return;
    const value = item.getAttribute('data-value');
    input.value = value;
    lastValue = value;
    closeList();
    input.focus();
    statusEl.innerHTML = `Selected: <strong>${value}</strong> — the input now holds the full value.`;
    (0, a11y_1.announce)(`${value} selected. Combobox closed.`);
}
function filterAndOpen() {
    const q = input.value.trim().toLowerCase();
    options = q
        ? CITIES.filter((c) => c.name.toLowerCase().includes(q))
        : CITIES.slice();
    options = options.slice(0, config_1.CONFIG.combobox.maxResults);
    listOpen = true;
    openList(true);
    (0, a11y_1.announce)(`${options.length} ${options.length === 1 ? 'result' : 'results'}.`);
}
// ---- input events ----
input.addEventListener('input', () => {
    // The revert target stays whatever it was when the list last opened
    // (captured on focus / first open), so Escape restores the pre-typing value.
    filterAndOpen();
});
input.addEventListener('focus', () => {
    lastValue = input.value;
    filterAndOpen();
});
input.addEventListener('blur', () => {
    closeList();
});
input.addEventListener('keydown', (e) => {
    const k = e.key;
    if (k === 'ArrowDown') {
        e.preventDefault();
        if (!listOpen) {
            lastValue = input.value; // opened by arrow: revert target is the text as-is
            filterAndOpen();
        }
        else {
            mgr.move(1);
        }
    }
    else if (k === 'ArrowUp') {
        e.preventDefault();
        if (listOpen)
            mgr.move(-1);
    }
    else if (k === 'Enter') {
        e.preventDefault();
        selectOption(mgr.current());
    }
    else if (k === 'Escape') {
        e.preventDefault();
        if (listOpen) {
            const revert = lastValue;
            closeList();
            input.value = revert; // revert (closeList already re-captured it)
            statusEl.innerHTML = `Closed — input reverted to <strong>“${revert}”</strong>.`;
            (0, a11y_1.announce)('Combobox closed, selection reverted.');
        }
    }
    else if (k === 'Tab') {
        closeList(); // let the browser move focus afterwards
    }
});
listbox.addEventListener('mousedown', (e) => {
    const opt = e.target.closest('[role="option"]');
    if (opt) {
        e.preventDefault(); // keep focus in the input
        selectOption(opt);
    }
});
function comboboxInit() {
    /* data-driven; listbox is populated on first interaction */
}
};

// ── module: src/components/accordion.ts ──
__mods["src/components/accordion.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.accordionInit = accordionInit;
/**
 * ============ 5. Accordion / disclosure group ============
 * Each trigger toggles its own region; multiple may be open.
 * Height animates first; `hidden` is only touched at the end of the
 * transition (or immediately under prefers-reduced-motion), so the
 * accessibility tree and the visual state never disagree.
 */
const dom_1 = require("../dom");
const a11y_1 = require("../a11y");
const config_1 = require("../config");
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
const DUR = config_1.CONFIG.accordionMs;
const triggers = (0, dom_1.$all)('.acc-trigger');
function setOpen(trigger, panel, opening, instant) {
    const expand = (done) => {
        panel.style.height = 'auto';
        done();
        panel.style.height = '';
    };
    const collapse = (done) => {
        panel.style.height = panel.scrollHeight + 'px';
        // force reflow, then animate to 0
        void panel.offsetHeight;
        panel.style.transition = `height ${DUR}ms ease`;
        panel.style.height = '0px';
        const finish = () => {
            panel.style.transition = '';
            panel.style.height = '';
            done();
        };
        panel.addEventListener('transitionend', finish, { once: true });
        // safety: if transitionend never fires (tab hidden), finish anyway
        window.setTimeout(finish, DUR + 80);
    };
    if (opening) {
        if (instant) {
            panel.removeAttribute('hidden');
            panel.style.height = '';
        }
        else {
            panel.removeAttribute('hidden');
            panel.style.height = '0px';
            void panel.offsetHeight;
            panel.style.transition = `height ${DUR}ms ease`;
            panel.style.height = 'auto';
            panel.addEventListener('transitionend', () => {
                panel.style.transition = '';
                panel.style.height = '';
            }, { once: true });
            window.setTimeout(() => {
                panel.style.transition = '';
                panel.style.height = '';
            }, DUR + 80);
        }
    }
    else {
        if (instant) {
            panel.style.height = '';
            panel.setAttribute('hidden', '');
        }
        else {
            collapse(() => panel.setAttribute('hidden', ''));
        }
    }
    trigger.setAttribute('aria-expanded', String(opening));
}
for (const trigger of triggers) {
    const panel = document.getElementById(trigger.getAttribute('aria-controls') || '');
    if (!panel)
        continue;
    trigger.addEventListener('click', () => {
        const opening = trigger.getAttribute('aria-expanded') !== 'true';
        setOpen(trigger, panel, opening, reduceMotion.matches);
        const name = trigger.textContent?.trim().replace(/\d+$/, '').trim();
        (0, a11y_1.announce)(`${name} ${opening ? 'expanded' : 'collapsed'}.`);
    });
}
function accordionInit() {
    /* triggers are static; listeners attached above */
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"dac0d684-bec9-4611-bebd-59e892df099a","tokensIn":2890125,"tokensOut":172267,"tokensTotal":3062392,"cost":0,"turns":66,"toolCalls":87,"failedToolCalls":3,"timestamp":"2026-08-15T18:03:46.147Z"} -->