/* DURATIONS COLLAPSED ONTO THE HOUSE LADDER, 2026-08-29.
   These were .26s and .3s, which added a fourth and fifth value to a system whose own
   design-system page says: twelve distinct durations ship, three are needed. The gallery's
   ladder is .12 / .18 / .28, derived by MEASURING Slab's 319 real transitions (76% are
   already .12s). settle and draw are now both .28s. The difference is 20ms — nobody can
   see it — and in exchange motion uses exactly the three durations the app already had. */
/* ============================================================================
   slab-motion.css — THE SHARED MOTION LAYER. Five primitives, one file.

   WHY A LAYER AND NOT PER-PAGE ANIMATION. Slab has already paid for the copied
   rule twice (sixteen `.main-hdr`s, four pricing rules). Motion is worse than
   most: a duration typed by hand in one page and guessed in the next reads as
   two different products on the same screen. So there are FIVE named things and
   nothing else, every page gets them from here, and a page that wants motion
   adds a class rather than a keyframe.

   HOW IT REACHES A PAGE. nav.js injects <link id="slab-motion-css"> once, beside
   the shared table system — so every page that mounts the nav has it. The mobile
   tree (m-*.html) does NOT load nav.js and must link /slab-motion.css itself.

   THE FIVE, AND THE ONE MEANING EACH CARRIES:
     .slab-press    the tap landed              — 0ms, never waits on a network
     .slab-rise     this is new on screen       — 12px, one direction
     .slab-settle   this row changed, here      — opacity wash, never a repaint
     .slab-wait     this is genuinely slow      — invisible unless it is
     .slab-draw     a SERVER said yes           — reserved; not "I clicked it"

   PLUS ONE, ADDED 2026-09-07: ENTER, the one-time sequence a mobile home plays
   the moment it is landed on straight from sign-in (never on reload, never on
   back). It is page-level rather than per-element like the five above, so it
   gets its own section (§6, near the bottom) instead of a bullet here.

   COLOUR. Tokens only, from slab-tokens.css, which declares every one twice
   (:root dark, html[data-theme="light"] light). No literal colour appears below.

   REDUCED MOTION is per-primitive, at the bottom of this file, and it is NOT the
   usual `*{animation:none}` blanket — that blanket would delete the one
   primitive whose entire job is telling a person what just changed.
   ========================================================================== */

:root{
  /* Four durations, because four is the number of distinct jobs. Anything that
     needs a fifth is probably two of these in sequence. */
  --dur-press:.12s;   /* the finger is still down; anything longer feels sticky */
  --dur-pop:.18s;     /* something arrived */
  --dur-move:.28s;    /* something changed in place */
  --dur-draw:.28s;     /* the check draws itself */

  /* --ease is symmetric (the material standard) for things that start and stop
     on screen. --ease-out decelerates into rest and is what an ARRIVING thing
     wants: fast off the mark, gentle at the end, so the eye lands with it. */
  --ease:cubic-bezier(.4,0,.2,1);
  --ease-out:cubic-bezier(.22,.61,.36,1);

  /* Added 2026-09-07 for the home-landing entrance (primitive 6, below). Two more
     durations, because the entrance is two different jobs — a page-level slide, then a
     small pop — and neither is any of the four above. --ease-pop OVERSHOOTS (its curve's
     output exceeds 1 mid-transition even though the keyframe ends exactly at scale(1)),
     which is deliberate and used nowhere else: everything else in this file arrives and
     stops, this one arrives, overshoots a hair, and settles — because it happens once, on
     load, nowhere near anyone's thumb. */
  --dur-enter:.5s;       /* the sheet/nav converge */
  --dur-enter-pop:.26s;  /* the avatar, then the logo square, popping in */
  --ease-pop:cubic-bezier(.34,1.56,.64,1);
}

/* ── 1. PRESS ──────────────────────────────────────────────────────────────
   The only primitive that fires at 0ms. It is the answer to "did that button
   hear me", and it must never be gated on a fetch — a press that waits for the
   server is a press that reports the server, not the tap. Transform only: the
   compositor moves it, so it stays smooth on the cheap Android in the field.
   Not for inline elements — transform does not apply to them; give the control
   `display:inline-flex` (every Slab button already has it).                  */
.slab-press{
  transition:transform var(--dur-press) var(--ease);
  -webkit-tap-highlight-color:transparent;
}
.slab-press:active{transform:scale(.97);}

/* ── 2. RISE ───────────────────────────────────────────────────────────────
   For content ARRIVING: a card list rendering, a panel opening, a row appended.
   Travel is 12px, not 6 — Adrienne raised it after reading both on a phone, and
   six is invisible at arm's length in daylight. One direction only (up), so a
   page never argues with itself about where new things come from.

   The class ships the BEFORE state; whatever renders the element adds `is-in`
   on the next frame (requestAnimationFrame, or after the node is in the DOM).  */
.slab-rise{
  opacity:0;
  transform:translateY(12px);
  transition:opacity var(--dur-pop) var(--ease-out),transform var(--dur-pop) var(--ease-out);
}
.slab-rise.is-in{opacity:1;transform:translateY(0);}

/* ── 3. SETTLE ─────────────────────────────────────────────────────────────
   "That saved, and it was THIS row." A green wash blooms and fades on a
   pseudo-element laid over the host — NEVER on the host's own background-color.
   Animating background-color repaints the element and everything composited
   with it on every frame, and on a mid-range Android that is visible chug on
   exactly the screens (long tables) where this primitive is most used. An
   overlay's opacity is a compositor property: free.

   `border-radius:inherit` keeps the wash inside a rounded card; the host needs
   `position:relative`, which this rule supplies rather than trusting each page
   to remember. Add `is-settled`, remove it on animationend to allow a re-fire. */
.slab-settle{position:relative;}
.slab-settle::after{
  content:"";
  position:absolute;
  inset:0;
  border-radius:inherit;
  pointer-events:none;
  background:var(--accent-green);
  opacity:0;
}
.slab-settle.is-settled::after{animation:slab-settle var(--dur-move) var(--ease-out);}
@keyframes slab-settle{0%{opacity:.16}100%{opacity:0}}

/* ── 4. WAIT ───────────────────────────────────────────────────────────────
   A spinner that appears for 200ms is noise: it reports the network rather than
   the work, and it makes a fast app look busy. So this one is INVISIBLE until
   the wait is genuinely slow. Under 900ms nothing is ever drawn — the animation
   is delayed with `both`, so the backwards fill holds opacity:0 through the
   delay and the fast path costs one composited layer that never paints.

   m-field already does this by hand; this is the same behaviour with a name.   */
.slab-wait{
  opacity:0;
  animation:slab-late var(--dur-pop) var(--ease-out) 900ms both;
}
@keyframes slab-late{from{opacity:0}to{opacity:1}}

/* ── 6. ENTER ──────────────────────────────────────────────────────────────
   Landing on a mobile home right after signing in, and ONLY then. m.html sets
   sessionStorage.slab_home_enter right before it location.replace()s into the
   home; each home reads that flag once, at the very start of its own boot(),
   and clears it immediately — so a reload or the phone's back button lands on
   the SAME markup with no flag, and plays nothing. This is the one primitive
   in the file that is not fired by a page's own JS logic elsewhere: it fires
   exactly once, at boot, or never.

   Adrienne, annotating a screenshot of the office home, 2026-09-07: "I would
   like the A to slide up, the background nav shape to slide down revealing
   the white elements. then I want C to pop in and then B to pop in." On every
   home, A is `.sheet`, the nav shape is `.topbar` + `.hero`, C is the avatar
   button (the subs' home has no avatar — `.slab-enter-c` lands on its
   equivalent, the language pill), B is the BFB square (`.m-logo .sq`).

   Three classes, added by JS only — never baked into any home's markup, so a
   home with no flag renders pixel-identical to before this existed:
     body.slab-entering   for the duration of the sequence
     .slab-enter-c        on the avatar / its equivalent
     .slab-enter-b        on the BFB square
   All three come off together, on the sheet's animationend (the longest
   animation in the sequence) — so nothing outlives it: no stuck transform, no
   held opacity, and .topbar keeps its own `position:sticky` throughout, since
   `transform` never participates in a sticky element's position calculation.

   Timing: the slide is 0–500ms. The avatar starts at 460ms (a hair before the
   slide settles — waiting for it to fully finish first reads as two separate
   beats instead of one), runs 260ms. The logo starts 120ms after the avatar,
   at 580ms, also 260ms — sequence ends around 840ms, "roughly 900ms" per the
   brief.                                                                      */
body.slab-entering .sheet{animation:slab-enter-up var(--dur-enter) var(--ease-out) both;}
body.slab-entering .topbar,
body.slab-entering .hero{animation:slab-enter-down var(--dur-enter) var(--ease-out) both;}
@keyframes slab-enter-up{0%{transform:translateY(36px)}100%{transform:translateY(0)}}
@keyframes slab-enter-down{0%{transform:translateY(-36px)}100%{transform:translateY(0)}}

.slab-enter-c{animation:slab-enter-pop var(--dur-enter-pop) var(--ease-pop) .46s both;}
.slab-enter-b{animation:slab-enter-pop var(--dur-enter-pop) var(--ease-pop) .58s both;}
@keyframes slab-enter-pop{0%{opacity:0;transform:scale(.4)}100%{opacity:1;transform:scale(1)}}

/* ── 5. DRAW ───────────────────────────────────────────────────────────────
   RESERVED FOR ONE MEANING: a server said yes. Not "I tapped save", not "the
   form validated" — the write came back. If it draws for anything else it stops
   meaning anything, and Slab loses the only signal that separates "sent" from
   "landed".

   `pathLength="1"` IS REQUIRED ON THE PATH. It renormalises the path's own
   length to 1, which is what lets `stroke-dasharray:1` mean "exactly one dash,
   the whole stroke" for a check of any geometry. Without it, dasharray:1 is one
   user-unit — a dotted line — and the primitive silently renders wrong:

     <path class="slab-draw" pathLength="1" d="M4 11l5 5L20 6"
           fill="none" stroke="currentColor" stroke-width="2.5"
           stroke-linecap="round" stroke-linejoin="round"/>                    */
.slab-draw{
  stroke-dasharray:1;
  stroke-dashoffset:1;
  transition:stroke-dashoffset var(--dur-draw) var(--ease-out);
}
.slab-draw.is-drawn{stroke-dashoffset:0;}

/* ── REDUCED MOTION ────────────────────────────────────────────────────────
   Decided primitive by primitive rather than swept away, because "no motion"
   and "no feedback" are not the same request. Vestibular triggers are TRAVEL
   and SCALE. Opacity is neither.                                              */
@media (prefers-reduced-motion:reduce){

  /* PRESS: the movement goes, the ANSWER stays. Dropping tap feedback entirely
     would be an accessibility regression wearing an accessibility fix's coat —
     the person who asked for less motion still needs to know the button heard
     them. brightness() is a filter: no layout, no travel, no scale.           */
  .slab-press{transition:filter var(--dur-press) var(--ease);}
  .slab-press:active{transform:none;filter:brightness(.9);}

  /* RISE: off. The element is simply present — no travel, no fade-in. `is-in`
     may or may not have been added yet, so both states must read as visible. */
  .slab-rise,.slab-rise.is-in{opacity:1;transform:none;transition:none;}

  /* SETTLE: STAYS ON. It moves nothing, scales nothing and carries no
     vestibular risk, and it is the one primitive whose whole job is "this
     changed, HERE" — the person who cannot watch a row slide is the person who
     most needs to be told which row it was. Slowed and HELD at the top so a
     still frame still reads it, then released. `!important` and the (0,2,1)
     specificity are deliberate: design-system.css carries a global
     `*{animation-duration:.001ms !important}` under this same media query,
     which would otherwise finish this animation before it was seen.           */
  .slab-settle.is-settled::after{
    animation:slab-settle-rm .9s linear !important;
  }
  @keyframes slab-settle-rm{0%,70%{opacity:.14}100%{opacity:0}}

  /* WAIT: the fade goes; the 900ms DELAY stays. The delay was never movement —
     it is the rule that keeps a fast response from flashing a spinner — so it
     survives, and the indicator simply appears rather than easing in.         */
  .slab-wait{animation:slab-late 1ms linear 900ms both;}

  /* DRAW: off. The check renders already complete — same answer, no stroke. */
  .slab-draw,.slab-draw.is-drawn{stroke-dashoffset:0;transition:none;}

  /* ENTER: off entirely — both TRAVEL (the slide) and SCALE (the pops) are
     exactly the two triggers this query exists for. The classes may already be
     sitting on the page when this is evaluated (a home doesn't check the media
     query before adding them, the CSS does the deciding) — that is fine, this
     unconditionally cancels the visual and leaves the final, resting layout,
     which is all `slab-entering`/`.slab-enter-c`/`.slab-enter-b` ever produce
     once the (skipped) sequence would have finished anyway. */
  body.slab-entering .sheet,
  body.slab-entering .topbar,
  body.slab-entering .hero,
  .slab-enter-c,
  .slab-enter-b{animation:none;transform:none;opacity:1;}
}
