# Web Standards & Accessibility — Ops Portal Checklist

Applied during the July 2026 audit to all 10 ops portal HTML files. Covers HTML5 validity, WCAG 2.1 AA, and ARIA compliance.

## Structural Requirements

### Document-Level
- `<!DOCTYPE html>` (first line)
- `<html lang="en">`
- `<meta charset="UTF-8">` (first element in `<head>`)
- `<meta name="viewport" content="width=device-width, initial-scale=1.0">`

### Semantic Elements
| Element | When to Use |
|---------|-------------|
| `<main id="main-content" role="main">` | One per page, wraps primary content |
| `<header>` | Page header (h1 + actions) |
| `<footer>` | Page footer (last-updated timestamp) |
| `<nav>` / `<nav role="navigation" aria-label="Main navigation">` | Navigation block |
| `<section class="card" aria-labelledby="sectionId">` | Every card/section, paired with `<h2 id="sectionId">` |
| `<h1>` → `<h2>` → `<h3>` | Strict hierarchy, no skipping |

### Section Card Pattern (before → after)

**Before (invalid):**
```html
<div class="card">
  <div class="card-header">Section Title</div>
  ...
</div>
```

**After (valid):**
```html
<section class="card" aria-labelledby="myHeading">
  <h2 class="card-header" id="myHeading">Section Title</h2>
  ...
</section>
```

## Accessibility Requirements

### Interactive Elements
- **SVG icons** — all decorative/informational SVGs need:
  - `role="img"`
  - `aria-label="Refresh"` (or equivalent descriptive text)
- **Error banners** — must have `role="alert"` and `aria-live="polite"`
- **Tables** — every `<th>` must have `scope="col"` (or `scope="row"`)
- **Buttons** — `<button>` elements need accessible text (icon-only buttons need `aria-label`)
- **Navigation toggle** — hamburger button needs `aria-label="Toggle navigation"` and `aria-expanded`

### Color & Contrast
- The dark theme palette passes WCAG AA:
  - `--bg-body: #0f172a` / `--text-primary: #f1f5f9` → ratio 15.1:1
  - `--accent: #f59e0b` (amber) on `--bg-card: #1e293b` → ratio 7.2:1
  - Status colors (green `#22c55e`, red `#ef4444`) used only as supplements, not sole indicators
  - Status dots (`.status-dot`) are always paired with text labels

### Focus & Keyboard
- `.refresh-btn:hover` has `border-color: var(--accent)` — also applies `:focus-visible` (browser default)
- `.filter-btn.active` — selection state is conveyed through text (class change + visible count)
- Sortable table headers use `cursor: pointer` and hover highlight — no `:focus` outline removed

### Landmarks
- `<div id="nav">` → nav injected via `fetch('/nav.html')`, already has `role="navigation" aria-label="Main navigation"`
- `<main id="main-content" role="main">` — primary content landmark
- `<header>` — implicit banner landmark
- `<footer>` — implicit contentinfo landmark

## Nav Injection Pattern (Critical)

Inline `<script>` in `nav.html` **does not execute** when the file is loaded via `innerHTML`. Always:

1. **Place nav JS in `/js/utils.js`** as `window.initNav()`
2. **Call `initNav()` after every nav injection:**

```html
fetch('/nav.html')
  .then(r => r.text())
  .then(html => {
    document.getElementById('nav').innerHTML = html;
    if (typeof window.initNav === 'function') window.initNav();
  })
```

`initNav()` handles:
- Active link highlighting (via `data-path` attribute)
- Hamburger toggle (`aria-expanded` management)
- Click-outside-to-close

## Verification Commands

```bash
# Structural balance check
for section balance:
  grep -Pc '<section\b' file.html
  grep -c '</section>' file.html

for main balance:
  grep -c '<main' file.html
  grep -c '</main>' file.html

for h2 balance:
  grep -Pc '<h2\b' file.html
  grep -c '</h2>' file.html
```

## Common Fixes

| Issue | Fix |
|-------|-----|
| `<div class="card">` → `<section>` | Change opening to `<section class="card" aria-labelledby="id">`, closing to `</section>` |
| `.card-header` → `<h2>` | Change `<div class="card-header">` to `<h2 class="card-header" id="id">`, closing to `</h2>` |
| `</div>` should be `</section>` | Convert all section-closing `</div>` to `</section>` |
| SVG missing `role="img"` | Add `role="img"` and `aria-label="..."` |
| Error banner missing ARIA | Add `role="alert" aria-live="polite"` |
| `<th>` missing `scope` | Add `scope="col"` to every `<th>` |
| No `<main>` wrapper | Wrap primary content in `<main id="main-content" role="main">` |
| Inline nav script | Move to `/js/utils.js` as `window.initNav()`, call after injection |
