/* ==========================================================================
   Animation library.

   Client requirement: every section animates DIFFERENTLY. One shared fade-up
   everywhere is explicitly a failure, so each effect is used by exactly ONE
   section and is named for what it does, not where it sits.

   ------------------------------------------------------------------------
   PATTERN — read this before adding an effect.

   Effects are ADDITIVE keyframe animations, never a hidden default state.

   Content is fully visible in CSS at all times. `reveal.js` adds `.is-in`,
   which RUNS an entrance animation; it never "unhides" anything. So if the
   script fails, is blocked, never fires, or the browser skips the observer
   entirely, the page is still completely readable — it simply does not
   animate.

   The alternative (hide by default, reveal with a class) means any scripting
   failure leaves a blank page. On a medical site where someone may be trying
   to find a treatment, that is not an acceptable failure mode, and it is why
   this file contains no `opacity: 0` default anywhere.

   `animation-fill-mode: both` holds the opening frame during any delay, so a
   staggered item does not flash before its turn.
   ------------------------------------------------------------------------

   Effect          Section
   --------------  ----------------------------------------------------
   rise            hero copy
   depth           hero decorative layer (scroll-linked)
   unfurl          hero contact card
   mask-wipe       specialties heading
   stagger-lift    specialties carousel
   count-in        journey steps (rule draw + number spring)
   ledger-slide    services index
   drift-in        destinations carousel
   zoom-settle     closing call to action

   flip-up is retained but currently unused: it belonged to the old services
   card grid, which is now the numbered index above. Kept because it is a
   distinct effect available to the next section that needs one.
   ========================================================================== */

/* --- rise — hero copy ----------------------------------------------------- */

@keyframes swl-rise {
  from { opacity: 0; translate: 0 18px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="rise"].is-in > [data-animate-item] {
  animation: swl-rise 620ms var(--ease) calc(var(--stagger, 0) * 90ms) both;
}

/* --- depth — hero decorative layer ---------------------------------------- */
/* Scroll-linked, not scroll-jacking: a small counter-shift giving the hero a
   sense of layers. --depth is written by reveal.js. */

.hero__aura {
  translate: 0 calc(var(--depth, 0) * 0.06px);
  scale: calc(1 + (var(--depth, 0) * 0.00012));
  will-change: translate, scale;
}

/* --- unfurl — hero contact card ------------------------------------------- */
/* Opens from its top edge, like a card being dealt face-up. */

@keyframes swl-unfurl {
  from { opacity: 0; rotate: x 12deg; }
  to   { opacity: 1; rotate: x 0deg; }
}

[data-animate="unfurl"].is-in {
  transform-origin: top center;
  animation: swl-unfurl 700ms var(--ease) 260ms both;
}

/* --- mask-wipe — specialties heading -------------------------------------- */
/* Revealed by a clip travelling left to right. */

@keyframes swl-mask-wipe {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

[data-animate="mask-wipe"].is-in {
  animation: swl-mask-wipe 900ms var(--ease) both;
}

/* --- stagger-lift — specialties grid -------------------------------------- */
/* Cards lift and settle in a cascade. */

@keyframes swl-stagger-lift {
  from { opacity: 0; translate: 0 26px; scale: 0.97; }
  to   { opacity: 1; translate: none; scale: 1; }
}

[data-animate="stagger-lift"].is-in > [data-animate-item] {
  animation: swl-stagger-lift 560ms var(--ease) calc(var(--stagger, 0) * 55ms) both;
}

/* --- count-in — journey --------------------------------------------------- */
/* The rule above each step draws itself, then the number springs in. Three
   coordinated animations rather than one, so the parts arrive in order. */

@keyframes swl-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes swl-draw {
  from { scale: 0 1; }
  to   { scale: 1 1; }
}

@keyframes swl-spring {
  from { scale: 0.4; rotate: -25deg; }
  to   { scale: 1; rotate: 0deg; }
}

[data-animate="count-in"].is-in > [data-animate-item] {
  animation: swl-fade 500ms var(--ease) calc(var(--stagger, 0) * 130ms) both;
}

[data-animate="count-in"].is-in > [data-animate-item] .journey__rule {
  transform-origin: left center;
  animation: swl-draw 700ms var(--ease) calc(var(--stagger, 0) * 130ms) both;
}

[data-animate="count-in"].is-in > [data-animate-item] .journey__number {
  animation: swl-spring 620ms cubic-bezier(.34, 1.56, .64, 1)
    calc(var(--stagger, 0) * 130ms + 120ms) both;
}

/* --- flip-up — services grid ---------------------------------------------- */
/* Each card pivots up from its lower edge. */

@keyframes swl-flip-up {
  from { opacity: 0; rotate: x -18deg; }
  to   { opacity: 1; rotate: x 0deg; }
}

[data-animate="flip-up"].is-in > [data-animate-item] {
  transform-origin: bottom center;
  animation: swl-flip-up 620ms var(--ease) calc(var(--stagger, 0) * 60ms) both;
}

/* --- drift-in — destinations grid ----------------------------------------- */
/* Cards drift in horizontally, alternating direction, like places arriving
   from either side of a map. */

@keyframes swl-drift-left {
  from { opacity: 0; translate: -28px 0; }
  to   { opacity: 1; translate: none; }
}

@keyframes swl-drift-right {
  from { opacity: 0; translate: 28px 0; }
  to   { opacity: 1; translate: none; }
}

[data-animate="drift-in"].is-in > [data-animate-item] {
  animation: swl-drift-left 560ms var(--ease) calc(var(--stagger, 0) * 65ms) both;
}

[data-animate="drift-in"].is-in > [data-animate-item]:nth-child(even) {
  animation-name: swl-drift-right;
}

/* --- zoom-settle — closing CTA -------------------------------------------- */
/* Arrives slightly oversized and settles, so the last thing on the page
   feels like it lands. */

@keyframes swl-zoom-settle {
  from { opacity: 0; scale: 1.06; }
  to   { opacity: 1; scale: 1; }
}

[data-animate="zoom-settle"].is-in {
  animation: swl-zoom-settle 900ms var(--ease) both;
}

/* --- Reduced motion ------------------------------------------------------- */
/* No entrance animation at all. Because nothing is hidden by default, simply
   cancelling the animation leaves every element correctly in place. */

@media (prefers-reduced-motion: reduce) {
  [data-animate].is-in,
  [data-animate].is-in > [data-animate-item],
  [data-animate].is-in > [data-animate-item] * {
    animation: none !important;
  }

  .hero__aura {
    translate: none;
    scale: 1;
  }
}

/* ==========================================================================
   Directory pages. Six further effects, none reused from the homepage.

   Effect          Page
   --------------  ----------------------------------------------------
   banner-sweep    every index banner
   shutter-open    treatments index
   tilt-settle     doctors index
   slide-reveal    hospitals index
   column-rise     treatment detail, body column
   ledger-drop     treatment detail, fact column
   fan-in          related treatments
   unfold          specialties index
   ========================================================================== */

/* --- banner-sweep - index banners ----------------------------------------- */

@keyframes swl-sweep {
  from { opacity: 0; translate: 0 -12px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="banner-sweep"].is-in > [data-animate-item] {
  animation: swl-sweep 560ms var(--ease) calc(var(--stagger, 0) * 80ms) both;
}

/* --- shutter-open - treatments index -------------------------------------- */
/* Rows open downward from their own top edge, like a shutter lifting. */

@keyframes swl-shutter {
  from { opacity: 0; scale: 1 0.82; }
  to   { opacity: 1; scale: 1 1; }
}

[data-animate="shutter-open"].is-in > [data-animate-item],
[data-animate="shutter-open"].is-in .treatment-row {
  transform-origin: top center;
  animation: swl-shutter 520ms var(--ease) calc(var(--stagger, 0) * 45ms) both;
}

/* --- tilt-settle - doctors index ------------------------------------------ */
/* Cards arrive slightly rotated and straighten, like photographs laid down. */

@keyframes swl-tilt {
  from { opacity: 0; rotate: -3.5deg; translate: 0 20px; }
  to   { opacity: 1; rotate: 0deg; translate: none; }
}

[data-animate="tilt-settle"].is-in > [data-animate-item] {
  animation: swl-tilt 600ms var(--ease) calc(var(--stagger, 0) * 60ms) both;
}

/* --- slide-reveal - hospitals index --------------------------------------- */
/* Each row is uncovered left to right. */

@keyframes swl-slide-reveal {
  from { opacity: 0; clip-path: inset(0 100% 0 0); }
  to   { opacity: 1; clip-path: inset(0 0 0 0); }
}

[data-animate="slide-reveal"].is-in > [data-animate-item] {
  animation: swl-slide-reveal 640ms var(--ease) calc(var(--stagger, 0) * 70ms) both;
}

/* --- column-rise - treatment detail --------------------------------------- */

@keyframes swl-column-rise {
  from { opacity: 0; translate: 0 24px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="column-rise"].is-in > [data-animate-item] {
  animation: swl-column-rise 640ms var(--ease) calc(var(--stagger, 0) * 100ms) both;
}

/* --- fan-in - related treatments ------------------------------------------ */
/* Cards fan out from a shared origin. */

@keyframes swl-fan {
  from { opacity: 0; rotate: 4deg; scale: 0.94; }
  to   { opacity: 1; rotate: 0deg; scale: 1; }
}

[data-animate="fan-in"].is-in > [data-animate-item] {
  transform-origin: bottom left;
  animation: swl-fan 560ms var(--ease) calc(var(--stagger, 0) * 85ms) both;
}

/* --- unfold - specialties index ------------------------------------------- */
/* Cards unfold from their centre line. */

@keyframes swl-unfold {
  from { opacity: 0; scale: 0.88 0.6; }
  to   { opacity: 1; scale: 1 1; }
}

[data-animate="unfold"].is-in > [data-animate-item] {
  animation: swl-unfold 560ms var(--ease) calc(var(--stagger, 0) * 50ms) both;
}

/* --- ledger-slide (homepage services) ------------------------------------- */
/* Each cell slides in from the inline start behind its own edge, one after
   the next, like rows being written into a ledger. Used only on the services
   index — no other section shares it (§54.14). */

/* ADDITIVE, like every other effect in this file. The first version of this
   used `opacity: 0` as the default state and revealed with `.is-in` — which
   is exactly what the pattern note above forbids, and it showed: the services
   block rendered as a blank white panel whenever the reveal had not fired
   yet. The cells are visible in CSS at all times; `.is-in` only runs the
   entrance. */

@keyframes ledger-slide {
  from {
    opacity: 0;
    transform: translate3d(-14px, 0, 0);
  }
}

[data-animate="ledger-slide"].is-in > [data-animate-item].is-in {
  animation: ledger-slide var(--transition-slow) both calc(var(--stagger) * 60ms);
}

@media (prefers-reduced-motion: reduce) {
  [data-animate="ledger-slide"].is-in > [data-animate-item].is-in {
    animation: none;
  }
}


/* --- ledger-drop (treatment detail, fact column) -------------------------- */
/* Settles down and in from the opposite side to the body column beside it, so
   the two columns read as arriving from different places rather than as one
   block. Used only on the treatment detail aside. */

@keyframes ledger-drop {
  from {
    opacity: 0;
    transform: translate3d(12px, -10px, 0);
  }
}

[data-animate="ledger-drop"].is-in > [data-animate-item].is-in {
  animation: ledger-drop var(--transition-slow) both calc(var(--stagger) * 80ms);
}

@media (prefers-reduced-motion: reduce) {
  [data-animate="ledger-drop"].is-in > [data-animate-item].is-in {
    animation: none;
  }
}


/* --- Settle guard --------------------------------------------------------- */
/* reveal.js adds `.is-settled` a few seconds after a section is revealed. It
   cancels the entrance animation so the content resolves to its natural,
   fully visible state.

   This covers the one case the additive pattern alone does not: an animation
   that is assigned but never advances. A frozen or throttled renderer, an
   embedded webview, or a tab that never composites will hold
   `animation-fill-mode: both` on its opening frame — which for most effects
   here is opacity 0 — and the section stays invisible indefinitely. The
   deadline is far longer than any effect in this file, so nothing is ever cut
   short in normal use. */

[data-animate].is-settled,
[data-animate].is-settled > [data-animate-item],
[data-animate].is-settled > [data-animate-item] * {
  animation: none !important;
}


/* ==========================================================================
   Editorial and contact pages. Seven further effects, none reused.

   Effect              Section
   ------------------  ------------------------------------------------
   mosaic-bloom        homepage gallery
   quote-rise          homepage testimonials
   accordion-cascade   homepage FAQ
   parallax-tilt       destinations index
   reveal-stack        destination detail
   pane-slide          contact page, form pane
   pane-counter        contact page, detail pane
   text-settle         legal pages

   Every one is additive and transform/opacity only, per the pattern note at
   the top of this file.
   ========================================================================== */

/* --- mosaic-bloom - homepage gallery -------------------------------------- */
/* Tiles bloom outward from slightly behind the plane, on a diagonal delay so
   the grid fills corner to corner rather than row by row. */

@keyframes swl-bloom {
  from { opacity: 0; scale: 0.88; }
  to   { opacity: 1; scale: 1; }
}

[data-animate="mosaic-bloom"].is-in > [data-animate-item] {
  animation: swl-bloom 620ms cubic-bezier(.22, 1, .36, 1)
    calc(var(--stagger, 0) * 45ms) both;
}

/* --- quote-rise - homepage testimonials ----------------------------------- */
/* Each quote lifts and un-skews, as though being set down onto the page. */

@keyframes swl-quote-rise {
  from { opacity: 0; translate: 0 30px; rotate: -1.2deg; }
  to   { opacity: 1; translate: none; rotate: 0deg; }
}

[data-animate="quote-rise"].is-in > [data-animate-item] {
  transform-origin: bottom left;
  animation: swl-quote-rise 700ms var(--ease) calc(var(--stagger, 0) * 110ms) both;
}

/* --- accordion-cascade - homepage FAQ ------------------------------------- */
/* Rows drop in from above one another, like a stack settling. Deliberately
   vertical and tight, so it reads as a list assembling rather than cards
   arriving. */

@keyframes swl-cascade {
  from { opacity: 0; translate: 0 -14px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="accordion-cascade"].is-in > [data-animate-item] {
  animation: swl-cascade 480ms var(--ease) calc(var(--stagger, 0) * 70ms) both;
}

/* --- parallax-tilt - destinations index ----------------------------------- */
/* Cards arrive tilted in perspective and level off — the only effect on the
   site using perspective, so the destinations page has a signature. */

@keyframes swl-parallax-tilt {
  from { opacity: 0; transform: perspective(900px) rotateY(-9deg) translate3d(0, 24px, -40px); }
  to   { opacity: 1; transform: perspective(900px) rotateY(0deg) translate3d(0, 0, 0); }
}

[data-animate="parallax-tilt"].is-in > [data-animate-item] {
  animation: swl-parallax-tilt 760ms cubic-bezier(.2, .8, .3, 1)
    calc(var(--stagger, 0) * 85ms) both;
}

/* --- reveal-stack - destination detail ------------------------------------ */
/* A clip opens downward while the content settles up into it, so the block
   assembles from the top edge. */

@keyframes swl-reveal-stack {
  from { opacity: 0; clip-path: inset(0 0 100% 0); translate: 0 16px; }
  to   { opacity: 1; clip-path: inset(0 0 0 0); translate: none; }
}

[data-animate="reveal-stack"].is-in > [data-animate-item] {
  animation: swl-reveal-stack 760ms var(--ease) calc(var(--stagger, 0) * 95ms) both;
}

/* --- pane-slide - contact page -------------------------------------------- */
/* The two panes enter from opposite sides and meet in the middle. */

@keyframes swl-pane-left {
  from { opacity: 0; translate: -34px 0; }
  to   { opacity: 1; translate: none; }
}

@keyframes swl-pane-right {
  from { opacity: 0; translate: 34px 0; }
  to   { opacity: 1; translate: none; }
}

[data-animate="pane-slide"].is-in > [data-animate-item] {
  animation: swl-pane-left 680ms var(--ease) calc(var(--stagger, 0) * 90ms) both;
}

[data-animate="pane-counter"].is-in > [data-animate-item] {
  animation: swl-pane-right 680ms var(--ease) calc(var(--stagger, 0) * 90ms) both;
}

/* --- text-settle - legal pages -------------------------------------------- */
/* Deliberately the quietest effect on the site. A privacy policy should not
   perform; it should simply arrive. */

@keyframes swl-text-settle {
  from { opacity: 0; translate: 0 8px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="text-settle"].is-in > [data-animate-item] {
  animation: swl-text-settle 520ms var(--ease) calc(var(--stagger, 0) * 120ms) both;
}

/* ==========================================================================
   ADDED 2026-08-08 — the four new sections, plus four effects that were named
   in Blade and defined nowhere.

   The four repairs first. `grid-bloom`, `stagger-in`, `rail-settle` and
   `profile-lift` were declared on real sections — the services index, the HTML
   sitemap, every detail-page rail and the doctor cards — but no stylesheet
   contained them, so reveal.js added `.is-in` and nothing happened. Four
   sections of the site did not animate at all, which is §54.14's headline
   requirement failing silently. Found while auditing effect names.
   ========================================================================== */

/* --- grid-bloom — services index ------------------------------------------ */
/* Cards scale up from slightly under full size, blooming out of the grid. */

@keyframes swl-grid-bloom {
  from { opacity: 0; scale: 0.94; }
  to   { opacity: 1; scale: 1; }
}

[data-animate="grid-bloom"].is-in > [data-animate-item] {
  animation: swl-grid-bloom 560ms var(--ease) calc(var(--stagger, 0) * 70ms) both;
}

/* --- stagger-in — HTML sitemap groups ------------------------------------- */
/* A long list of links entering from the reading edge, one after another. */

@keyframes swl-stagger-in {
  from { opacity: 0; translate: -14px 0; }
  to   { opacity: 1; translate: none; }
}

[data-animate="stagger-in"].is-in > [data-animate-item] {
  animation: swl-stagger-in 460ms var(--ease) calc(var(--stagger, 0) * 45ms) both;
}

/* --- rail-settle — detail-page side rail ---------------------------------- */
/* Arrives from the outside edge and settles, so the rail reads as docking
   beside the body column rather than rising with it. */

@keyframes swl-rail-settle {
  from { opacity: 0; translate: 26px 10px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="rail-settle"].is-in > [data-animate-item] {
  animation: swl-rail-settle 700ms var(--ease) calc(var(--stagger, 0) * 100ms) both;
}

/* --- profile-lift — doctor cards ------------------------------------------ */
/* Lifts from the bottom edge with a slight scale, the way a portrait card is
   picked up off a table. */

@keyframes swl-profile-lift {
  from { opacity: 0; translate: 0 22px; scale: 0.97; }
  to   { opacity: 1; translate: none; scale: 1; }
}

[data-animate="profile-lift"].is-in > [data-animate-item] {
  transform-origin: bottom center;
  animation: swl-profile-lift 620ms var(--ease) calc(var(--stagger, 0) * 75ms) both;
}

/* --- story-unwrap — the About body ---------------------------------------- */
/* A clip opens from the reading edge across the column, like a page being
   unrolled. One block, so no stagger. */

@keyframes swl-story-unwrap {
  from { opacity: 0; clip-path: inset(0 100% 0 0); }
  to   { opacity: 1; clip-path: inset(0 0 0 0); }
}

[data-animate="story-unwrap"].is-in {
  animation: swl-story-unwrap 820ms var(--ease) both;
}

/* --- step-ladder — how treatment abroad runs ------------------------------ */
/* Each step climbs in from below its predecessor, with a longer gap between
   them than anywhere else on the site: the point of the section is that these
   happen in sequence, and the motion says so. */

@keyframes swl-step-ladder {
  from { opacity: 0; translate: 0 26px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="step-ladder"].is-in > [data-animate-item] {
  animation: swl-step-ladder 600ms var(--ease) calc(var(--stagger, 0) * 140ms) both;
}

/* --- tile-cascade — what we arrange --------------------------------------- */
/* Tiles drop in from above in quick succession. */

@keyframes swl-tile-cascade {
  from { opacity: 0; translate: 0 -18px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="tile-cascade"].is-in > [data-animate-item] {
  animation: swl-tile-cascade 520ms var(--ease) calc(var(--stagger, 0) * 55ms) both;
}

/* --- map-settle — where we coordinate treatment --------------------------- */
/* Places settle onto the page with a small overshoot, like pins dropping onto
   a map. */

@keyframes swl-map-settle {
  from { opacity: 0; translate: 0 -22px; scale: 1.04; }
  60%  { opacity: 1; translate: 0 4px; scale: 1; }
  to   { opacity: 1; translate: none; scale: 1; }
}

[data-animate="map-settle"].is-in > [data-animate-item] {
  animation: swl-map-settle 680ms var(--ease) calc(var(--stagger, 0) * 80ms) both;
}

/* --- portrait-rise — patient stories on the About page -------------------- */
/* A straight, slower lift, so the last section of the page reads calmer than
   the ones above it. */

@keyframes swl-portrait-rise {
  from { opacity: 0; translate: 0 30px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="portrait-rise"].is-in > [data-animate-item] {
  animation: swl-portrait-rise 720ms var(--ease) calc(var(--stagger, 0) * 110ms) both;
}

/* --- pledge-lift — the About closing call to action ----------------------- */
/* The same CTA component as the homepage, deliberately a different motion:
   §54.14 is about the effect, not the markup. */

@keyframes swl-pledge-lift {
  from { opacity: 0; translate: 0 24px; rotate: x 6deg; }
  to   { opacity: 1; translate: none; rotate: x 0deg; }
}

[data-animate="pledge-lift"].is-in > [data-animate-item] {
  transform-origin: bottom center;
  animation: swl-pledge-lift 660ms var(--ease) calc(var(--stagger, 0) * 90ms) both;
}

/* --- card-deal — the packages listing ------------------------------------- */
/* Dealt in from the inline start with a slight rotation, like cards off the
   top of a deck. */

@keyframes swl-card-deal {
  from { opacity: 0; translate: -26px 12px; rotate: -2.5deg; }
  to   { opacity: 1; translate: none; rotate: 0deg; }
}

[data-animate="card-deal"].is-in > [data-animate-item] {
  animation: swl-card-deal 620ms var(--ease) calc(var(--stagger, 0) * 85ms) both;
}

/* --- panel-open — a package's body column --------------------------------- */
/* Each block opens from its own top edge, so the included / not-included
   panels read as folding open rather than sliding in. */

@keyframes swl-panel-open {
  from { opacity: 0; clip-path: inset(0 0 60% 0); translate: 0 10px; }
  to   { opacity: 1; clip-path: inset(0 0 0 0); translate: none; }
}

[data-animate="panel-open"].is-in > [data-animate-item] {
  animation: swl-panel-open 700ms var(--ease) calc(var(--stagger, 0) * 85ms) both;
}

/* --- deck-shuffle — other packages ---------------------------------------- */
/* Alternating rotation, so the row shuffles rather than marches. */

@keyframes swl-deck-shuffle-a {
  from { opacity: 0; translate: 0 20px; rotate: 1.5deg; }
  to   { opacity: 1; translate: none; rotate: 0deg; }
}

@keyframes swl-deck-shuffle-b {
  from { opacity: 0; translate: 0 20px; rotate: -1.5deg; }
  to   { opacity: 1; translate: none; rotate: 0deg; }
}

[data-animate="deck-shuffle"].is-in > [data-animate-item] {
  animation: swl-deck-shuffle-a 560ms var(--ease) calc(var(--stagger, 0) * 70ms) both;
}

[data-animate="deck-shuffle"].is-in > [data-animate-item]:nth-child(even) {
  animation-name: swl-deck-shuffle-b;
}

/* --- page-turn — the articles listing ------------------------------------- */
/* Each card turns in around its inline-start edge, like a page being turned.
   The origin mirrors under RTL with the rest of the layout. */

@keyframes swl-page-turn {
  from { opacity: 0; rotate: y -14deg; translate: 12px 0; }
  to   { opacity: 1; rotate: y 0deg; translate: none; }
}

[data-animate="page-turn"].is-in > [data-animate-item] {
  transform-origin: left center;
  perspective: 900px;
  animation: swl-page-turn 640ms var(--ease) calc(var(--stagger, 0) * 80ms) both;
}

[dir="rtl"] [data-animate="page-turn"].is-in > [data-animate-item] {
  transform-origin: right center;
}

/* --- type-set — an article's own column ----------------------------------- */
/* A long stagger on a small offset, so the column composes itself the way set
   type falls into a frame. */

@keyframes swl-type-set {
  from { opacity: 0; translate: 0 12px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="type-set"].is-in > [data-animate-item] {
  animation: swl-type-set 560ms var(--ease) calc(var(--stagger, 0) * 130ms) both;
}

/* --- shelf-slide — more to read ------------------------------------------- */
/* Slides in horizontally as one row, like books pushed along a shelf. */

@keyframes swl-shelf-slide {
  from { opacity: 0; translate: 34px 0; }
  to   { opacity: 1; translate: none; }
}

[data-animate="shelf-slide"].is-in > [data-animate-item] {
  animation: swl-shelf-slide 600ms var(--ease) calc(var(--stagger, 0) * 60ms) both;
}

/* --- quill-lift — the article closing call to action ---------------------- */

@keyframes swl-quill-lift {
  from { opacity: 0; translate: 0 18px; scale: 0.98; }
  to   { opacity: 1; translate: none; scale: 1; }
}

[data-animate="quill-lift"].is-in > [data-animate-item] {
  animation: swl-quill-lift 620ms var(--ease) calc(var(--stagger, 0) * 100ms) both;
}

/* --- portrait-fan — the stories listing ----------------------------------- */
/* Fans open from a shared bottom origin, like photographs spread on a table.
   Distinct from portrait-rise, which is a straight lift with no rotation. */

@keyframes swl-portrait-fan {
  from { opacity: 0; translate: 0 24px; rotate: 3deg; }
  to   { opacity: 1; translate: none; rotate: 0deg; }
}

[data-animate="portrait-fan"].is-in > [data-animate-item] {
  transform-origin: bottom center;
  animation: swl-portrait-fan 660ms var(--ease) calc(var(--stagger, 0) * 95ms) both;
}

/* --- letter-open — a story's own column ----------------------------------- */
/* Unfolds from the top edge, the way a letter is opened. Deliberately gentle:
   this is somebody's account of an illness, not a product page. */

@keyframes swl-letter-open {
  from { opacity: 0; clip-path: inset(0 0 45% 0); translate: 0 8px; }
  to   { opacity: 1; clip-path: inset(0 0 0 0); translate: none; }
}

[data-animate="letter-open"].is-in > [data-animate-item] {
  animation: swl-letter-open 740ms var(--ease) calc(var(--stagger, 0) * 120ms) both;
}

/* --- album-turn — other stories ------------------------------------------- */

@keyframes swl-album-turn {
  from { opacity: 0; rotate: y 12deg; translate: -10px 0; }
  to   { opacity: 1; rotate: y 0deg; translate: none; }
}

[data-animate="album-turn"].is-in > [data-animate-item] {
  transform-origin: right center;
  perspective: 900px;
  animation: swl-album-turn 620ms var(--ease) calc(var(--stagger, 0) * 75ms) both;
}

[dir="rtl"] [data-animate="album-turn"].is-in > [data-animate-item] {
  transform-origin: left center;
}

/* --- hand-raise — the story closing call to action ------------------------ */

@keyframes swl-hand-raise {
  from { opacity: 0; translate: 0 28px; }
  55%  { opacity: 1; translate: 0 -4px; }
  to   { opacity: 1; translate: none; }
}

[data-animate="hand-raise"].is-in > [data-animate-item] {
  animation: swl-hand-raise 700ms var(--ease) calc(var(--stagger, 0) * 110ms) both;
}

/* --- beacon-in — the search results list ----------------------------------- */
/* Every other entrance in this file arrives from BELOW or from the side; a
   search result arrives from slightly LARGER and settles into place, the way
   a match snaps into focus when it is pointed out. Transform and opacity
   only, per §12. Used ONLY on the search results page. */

@keyframes swl-beacon-in {
  from { opacity: 0; scale: 1.03; }
  60%  { opacity: 1; }
  to   { opacity: 1; scale: 1; }
}

[data-animate="beacon-in"].is-in > [data-animate-item] {
  animation: swl-beacon-in 480ms var(--ease) calc(var(--stagger, 0) * 65ms) both;
}
