CAROUSEL: Scroll-snap photo carousel for school/provider galleries
VARIANTS: Track (arrows + dots) | Grid + Lightbox | Single image (controls hidden)

CSS scroll-snap drives the motion — carousel_controller.js only computes arrow/dot scrollTo targets and syncs active-slide state via an IntersectionObserver (no scroll-event polling, no third-party carousel library). Arrows disable at the ends rather than wrapping around (see the component doc's "Ends behavior" section). Images below are inline SVG placeholders (no external network calls) — swap for real `<img src="...">` in production use.

Track carousel — school gallery (5 photos)

Arrows + dots: click an arrow, click a dot, or focus the track and use Left/Right. The active dot gets aria-current="true"; arrows disable (native disabled) at the first/last photo. An sr-only status region announces "Photo N of M: caption" on every change.

<div data-controller="carousel"><div class="carousel"><div class="carousel-track" data-carousel-target="track" tabindex="0" data-action="keydown->carousel#keydown"><div class="carousel-slide" data-carousel-target="slide"><img src="..." alt="..."><div class="carousel-caption">Main Building</div></div>...</div><button class="carousel-arrow-prev" data-carousel-target="prev" data-action="click->carousel#prev">...</button><button class="carousel-arrow-next" data-carousel-target="next" data-action="click->carousel#next">...</button></div><div class="carousel-dots" data-carousel-target="dots"><button class="carousel-dot" data-carousel-target="dot" data-action="click->carousel#goToDot"></button>...</div></div>

Grid + Lightbox

Grid variant: a plain thumbnail grid — no scroll-snap track, no carousel_controller.js. Each thumbnail is its own independent data-controller="popup" instance; clicking it opens a .carousel-lightbox (a dark, minimal-chrome .popup-panel modifier) with the enlarged photo. Escape and backdrop-click close it — the popup contract handles both, unchanged.

<div data-controller="popup"><button class="carousel-grid-item" data-action="click->popup#show"><img src="..." alt="..."></button><div class="popup-overlay" data-popup-target="overlay" data-action="click->popup#handleOverlayClick"></div><div class="popup-panel popup-lg carousel-lightbox" data-popup-target="panel" role="dialog" aria-modal="true">...<div class="popup-body"><img src="..." alt="..."></div></div></div>

Single image — controls hidden

One slide, nothing to navigate to: with a single .carousel-slide, carousel_controller.js hides the arrows and the dots row on connect (native hidden attribute) instead of showing disabled-forever controls.

<!-- one .carousel-slide inside .carousel-track — arrows/dots hidden on connect() -->