Ads
Placeholder ad units that behave like real ones: a skeleton until a slot is actually on screen, a lazy load that waits for that, and a hide switch that belongs to the reader.
Every slot below starts as .skeleton.skeleton-breathe — the same shimmer this system already uses for unknown-shape content — then swaps to its placeholder creative once src/ad.js decides it has "loaded". Nothing here calls a real ad network; the point is the mechanism, not the inventory.
Sizes
IAB standard units, each capped by max-width so a desktop leaderboard still fits a phone rather than overflowing it.
.ad-leaderboard — 728 × 90
.ad-rectangle — 300 × 250, the most-served unit on the web
.ad-skyscraper (160 × 600) and .ad-mobile-banner (320 × 50)
.ad-responsive — full width, a floor height, no fixed ratio
Animate on load
The .ad-animate variant rises the creative in rather than just cross-fading it — the same fx-rise every card entrance already uses, not a new keyframe.
.ad-animate — refresh the page to see it fire
Lazy load, skeleton, and the hide switch
| Hook | Does |
|---|---|
data-ad | marks a slot for src/ad.js to manage — nothing renders without it |
data-ad-state | "idle" → "loading" → "loaded", or "hidden" — the only thing the script writes |
IntersectionObserver | a slot only starts "loading" once it is within 200px of the viewport |
data-ad-hide | on the close button — sets data-ad-state="hidden", the slot collapses out of layout |
No IntersectionObserver support | every slot loads immediately — a slower reveal, never a missing ad |
.ad-animate rise is skipped entirely under prefers-reduced-motion — the creative still appears, it just stops moving, the same rule every other entrance in this system follows.