{# Composable click-through carousel. One widget, used everywhere. A carousel is just an ordered list of frames; every surface (app product pages, landing) calls this same macro with a different frame list. Do not fork it for bespoke layouts, if a surface needs more, extend the macro. Click-through, not animated: the viewer advances frames with the prev/next controls or the arrow keys. No autoplay, no auto-advance, no slide animation, frames swap instantly. The dots are gone as of 2026-08-14. A dot strip has no terminal form and no good webview form past a handful of frames, and only the landing gallery is a fixed three -- the project and item galleries are creator uploads of any length. The arrows came off the picture in the same pass; nothing overlays a frame now. Each frame is a `CarouselFrame { image, alt, caption: Option<_> }` (see `src/templates/mod.rs`). Usage is unchanged: {%- import "partials/carousel.html" as carousel -%} {% call carousel::carousel("af-shots", frames) %}{% endcall %} THE MARKUP IS NO LONGER WRITTEN HERE. It comes from the description layer: `crate::quasi::widgets::carousel` says "an ordered set of pictures, called a carousel" and quasi-webview turns that into markup. This file is now the call, and the reason it still exists is that three pages already import it. What that buys, beyond one description instead of one template: - The frame's edge is a bevel from makeover, not a border and an rgba() shadow written by hand here. In-flow depth is a bevel; elevation is for what sits over the page. - Progressive enhancement runs the other way, and costs nothing to do it. This used to render frame one and leave the other frames unreachable without JS, because the controls that reach them are the scripted part. The description now carries the whole gallery, but the DEFAULT rendering shows one frame, so a visitor with JavaScript sees a settled page from first paint. `no-js.css`, loaded from the