/* lc-motion.css — the platform's one motion kernel.
   Every keyframe on the site lives here. Verticals opt in with classes; nobody
   defines a private spinner. Companion: lc-motion.js (window.LCMotion), enqueued
   by mu-plugins/lc-motion.php.

   The governing rule, from docs/superpowers/specs/2026-08-02-motion-system-design.md:
   a wait has three lengths and each needs a different answer —
     under 200ms  show nothing (a loader that flashes reads as a glitch, not as speed)
     200ms – 1s   show the SHAPE of what is coming, so nothing jumps on arrival
     over 1s      NAME the thing being fetched, which turns waiting into anticipation
   The mega menu learned this the hard way; see the note at lc-megamenu.css:117. */

/* ---------------------------------------------------------------- tokens -- */
:root {
  --lc-ease:   cubic-bezier(.22,.72,0,1);
  /* one overshoot-and-settle curve, used anywhere something should read as
     having mass — sheets, saved hearts, dropped map pins. */
  --lc-spring: linear(0,.4 8%,.82 17%,1.02 24%,1.06 30%,1.02 40%,.996 62%,1);

  --lc-t-fast: .18s;   /* hover menus — never make a fast mouse wait */
  --lc-t-base: .32s;
  --lc-t-slow: .55s;

  --lc-gate:   200ms;  /* specimen 04 — the wait a loader must survive to paint */
  --lc-stagger-step: 70ms;

  --lc-skel:      #E7E2E1;
  --lc-skel-high: #F3EEED;
  --lc-rose:      #CC9999;
}

/* @property lets the shimmer's travel and the roll counter animate as real typed
   values rather than being stuck at their initial state. */
@property --lc-sweep  { syntax: '<length-percentage>'; initial-value: -120%; inherits: false; }
@property --lc-num    { syntax: '<integer>';           initial-value: 0;     inherits: false; }

/* =========================================================== 04 · the gate ==
   .lc-pending paints nothing for the first 200ms. Applied by LCMotion.pending()
   to whatever wrapper is waiting. If the response beats the gate the visitor
   never sees a loading state at all, which is the entire point — fast should
   look instant, not busy. */
.lc-pending { position: relative; }
.lc-pending > .lc-pending-veil {
  position: absolute; inset: 0; z-index: 6;
  display: flex; flex-direction: column; gap: 10px;
  background: var(--lc-bg, #fff);
  opacity: 0;
  animation: lc-gate-in var(--lc-t-base) var(--lc-ease) var(--lc-gate) both;
}
/* A first load has an EMPTY container, so an absolutely-positioned veil would
   stretch over a zero-height box and show nothing at all — the exact failure the
   skeleton exists to prevent. When there is nothing to cover, the veil sits in
   normal flow instead and gives the container its height. lc-motion.js decides
   which case applies. */
.lc-pending > .lc-pending-veil.is-flow { position: static; min-height: var(--lc-pending-min, 240px); }
/* The label is absolutely positioned, so without this it sits on top of the first
   ghost row instead of above it. */
.lc-pending > .lc-pending-veil.has-label { padding-top: 26px; }
@keyframes lc-gate-in { from { opacity: 0 } to { opacity: 1 } }

/* ======================================================= 01 · content ghost ==
   Grey blocks in the exact shape the real cards will take. Never fake headlines
   and never fake prices — a placeholder that looks like content is a lie the
   visitor notices the moment it resolves into something else. */
.lc-skel-grid {
  display: grid; gap: var(--lc-skel-gap, 18px);
  grid-template-columns: repeat(auto-fill, minmax(var(--lc-skel-min, 240px), 1fr));
}
.lc-skel-card {
  position: relative; overflow: hidden; border-radius: 3px;
  background: var(--lc-card-bg, #fff);
  display: flex; flex-direction: column;
}
.lc-skel-img   { aspect-ratio: 4/3; background: var(--lc-skel); }
.lc-skel-body  { padding: 10px 12px 14px; display: flex; flex-direction: column; gap: 7px; }
.lc-skel-line  { height: 9px; border-radius: 99px; background: var(--lc-skel); }
.lc-skel-line.is-short { width: 54%; }
.lc-skel-line.is-mid   { width: 76%; }

/* Row shape — a thumbnail beside two lines, for the directories whose results
   are a vertical list rather than a card grid (shops, schools, restaurants).
   Matching the real card is not a detail, it IS the feature: a ghost of the
   wrong shape still lets the layout jump on arrival, which is the one thing a
   skeleton exists to prevent. Sizes come from the vertical via custom props so
   the kernel never hardcodes one directory's dimensions. */
.lc-skel-grid.is-rows { grid-template-columns: 1fr; }
.lc-skel-card.is-row {
  display: grid; grid-template-columns: var(--lc-skel-thumb, 132px) 1fr;
  gap: 14px; padding: 12px; align-items: start;
}
.lc-skel-card.is-row .lc-skel-img {
  aspect-ratio: auto; height: var(--lc-skel-thumb-h, 104px); border-radius: 4px;
}
.lc-skel-card.is-row .lc-skel-body { padding: 6px 0 0; }

/* Most directory cards carry a hairline frame. Without it the ghost is a
   borderless slab and a border materialises on arrival — a small jump, but a
   jump, and this whole primitive exists to prevent exactly that. */
.lc-skel-card.has-frame {
  border: 1px solid var(--lc-line, #e3e6e3);
  border-radius: var(--lc-skel-radius, 6px);
}

/* The light pass. Warm rather than grey — a neutral sweep is the tell that says
   "generic template". Travel is a transform so it stays on the compositor. */
.lc-skel-card::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(100deg,
    transparent 34%, var(--lc-skel-high) 50%, transparent 66%);
  transform: translateX(var(--lc-sweep));
  animation: lc-shine 1.9s linear infinite;
  animation-delay: calc(var(--lc-i, 0) * 90ms);
  opacity: .9;
}
@keyframes lc-shine { from { --lc-sweep: -120% } to { --lc-sweep: 120% } }

/* ======================================================== 02 · name the thing ==
   Past a second, silence reads as broken. The label is real: a real noun and a
   real count, written by whoever called LCMotion.pending(). It only appears once
   the wait has run long enough to deserve an explanation. */
.lc-pending-label {
  position: absolute; left: 0; top: 0; z-index: 7;
  padding-left: 20px;
  font: 700 10px/1.6 var(--lc-mono, ui-monospace, Consolas, monospace);
  letter-spacing: .22em; text-transform: uppercase;
  color: currentColor; opacity: 0;
  animation: lc-gate-in var(--lc-t-base) var(--lc-ease) 1s both;
}
.lc-pending-label::before {
  content: ''; position: absolute; left: 0; top: 4px;
  width: 9px; height: 9px; border-radius: 99px; background: var(--lc-rose);
  animation: lc-seek .9s var(--lc-ease) infinite;
}
@keyframes lc-seek {
  0%, 100% { transform: translateX(0) scale(1);      opacity: .25 }
  50%      { transform: translateX(7px) scale(1.35); opacity: 1   }
}

/* ============================================================ 03 · map settle ==
   Markers land over ~400ms instead of all in one frame. LCMotion.stagger() caps
   how many get a delay — 200 pins cannot each wait their turn, so everything past
   the cap arrives immediately with the last delayed batch. */
.lc-pin-in {
  animation: lc-drop var(--lc-t-slow) var(--lc-spring) both;
  animation-delay: calc(var(--lc-i, 0) * 55ms);
}
@keyframes lc-drop {
  from { transform: translateY(-18px) scale(.4); opacity: 0 }
  to   { transform: none;                        opacity: 1 }
}

/* ==================================================== 05 · concierge thinking ==
   The caret belongs to the stream. A blinking caret left on finished text reads
   as broken, so lc-motion.js removes .lc-typing the moment the stream closes. */
.lc-think { display: inline-flex; gap: 5px; align-items: center; }
.lc-think i {
  width: 6px; height: 6px; border-radius: 99px; background: var(--lc-rose);
  animation: lc-breathe 1.25s var(--lc-ease) infinite;
  animation-delay: calc(var(--lc-i, 0) * .16s);
}
@keyframes lc-breathe {
  0%, 100% { opacity: .28; transform: translateY(0)    }
  45%      { opacity: 1;   transform: translateY(-3px) }
}
.lc-typing { border-right: 1.5px solid var(--lc-rose); animation: lc-caret .7s steps(1) infinite; }
@keyframes lc-caret { 50% { border-color: transparent } }

/* ====================================================== 06 · staggered arrival ==
   Cards rise ~70ms apart. Short enough that nobody consciously notices, long
   enough that the eye reads a wave instead of a flash. Only the first row is
   staggered — see the cap in LCMotion.stagger(). */
.lc-arrive > * {
  animation: lc-rise var(--lc-t-slow) var(--lc-ease) both;
  animation-delay: calc(var(--lc-i, 0) * var(--lc-stagger-step));
}
@keyframes lc-rise {
  from { opacity: 0; transform: translateY(14px) }
  to   { opacity: 1; transform: none             }
}

/* @starting-style gives the same entrance to nodes appended later (infinite
   scroll, streamed results) with no JS bookkeeping at all. */
@supports (transition-behavior: allow-discrete) {
  .lc-arrive-auto > * {
    opacity: 1; transform: none;
    transition: opacity var(--lc-t-base) var(--lc-ease),
                transform var(--lc-t-base) var(--lc-ease);
  }
  @starting-style {
    .lc-arrive-auto > * { opacity: 0; transform: translateY(14px) }
  }
}

/* ========================================================== 08 · count roll ==
   Pure CSS: an animated integer read back through counter(). Kept under 350ms —
   a number theatrically counting to 193 is a toy, not feedback.

   Named .lc-roll, NOT .lc-count: every directory already uses .lc-count for its
   result line, and squatting on it would have appended a stray counter to all of
   them. Same reason .lc-press is not .lc-btn and .lc-bloom is not .lc-saved. */
.lc-roll { font-variant-numeric: tabular-nums; counter-reset: lcn var(--lc-num); }
.lc-roll::after { content: counter(lcn); }
.lc-roll.is-rolling { animation: lc-roll-to .3s var(--lc-ease) forwards; }
@keyframes lc-roll-to { from { --lc-num: var(--lc-from, 0) } to { --lc-num: var(--lc-to, 0) } }

/* ========================================================= 09 · photo bloom ==
   Images resolve out of the placeholder's own tone rather than snapping in one
   frame late. Only applied to images that actually had to travel — a bloom on a
   cached image reads as lag, so lc-motion.js skips anything already complete. */
.lc-photo-in { animation: lc-bloom var(--lc-t-slow) var(--lc-ease) both; }
@keyframes lc-bloom {
  from { opacity: 0; filter: saturate(.15) blur(6px); transform: scale(1.04) }
  to   { opacity: 1; filter: none;                    transform: none        }
}

/* ==================================================== 10/11 · view transitions ==
   Cross-document navigation opt-in lives in lc-motion.php so it can be switched
   off per-template. Browsers without support simply navigate — nothing here needs
   a fallback. The root cross-fade is deliberately quick; a slow page fade feels
   like latency rather than polish. */
::view-transition-old(root),
::view-transition-new(root) { animation-duration: var(--lc-t-base); }

/* 11 · a card's photo grows into the hero of the page it opens. Both pages must
   agree on the name — set per vertical, never globally. */
::view-transition-group(lc-hero) { animation-timing-function: var(--lc-ease); }

/* ========================================================= 12 · menu unfurl ==
   For the REBUILT mega menus (owner is mid-rebuild, 2026-08-02) — not a patch on
   the current ones. Wipes down from its own top edge, links following just behind. */
.lc-unfurl { animation: lc-unfurl var(--lc-t-fast) var(--lc-ease) both; }
@keyframes lc-unfurl {
  from { clip-path: inset(0 0 100% 0); transform: translateY(-6px); opacity: .4 }
  to   { clip-path: inset(0 0 0 0);    transform: none;             opacity: 1  }
}

/* ====================================================== 13 · sheet with weight ==
   One change lands on all thirteen directories at once — every mobile filter
   routes through LCBottomBar. The overshoot must never reveal background beneath
   the sheet, hence the padded pseudo-element rather than a taller transform. */
.lc-sheet-in { animation: lc-sheet .62s var(--lc-spring) both; }
.lc-sheet-in::after {
  content: ''; position: absolute; inset: 100% 0 -40px 0;
  background: inherit; pointer-events: none;
}
@keyframes lc-sheet { from { transform: translateY(100%) } to { transform: translateY(0) } }

/* =========================================================== 14 · save bloom ==
   Saving is the most valuable action on the platform — it is the whole basis of
   accounts and return visits — so it should be the most satisfying thing to do.
   Fires optimistically; lc-saves reverses it honestly if the request fails. */
/* position:relative so the ring has something to anchor to. Safe on any button —
   relative with no offsets moves nothing — and necessary because the save control
   is different markup in every vertical. */
.lc-bloom { position: relative; animation: lc-pop .5s var(--lc-spring) both; }
@keyframes lc-pop {
  0%   { transform: scale(1)    }
  40%  { transform: scale(1.32) }
  100% { transform: scale(1)    }
}
.lc-bloom::after {
  content: ''; position: absolute; inset: -2px; border-radius: inherit;
  border: 1.5px solid var(--lc-rose); pointer-events: none;
  animation: lc-ring .7s var(--lc-ease) both;
}
@keyframes lc-ring {
  from { transform: scale(.7);  opacity: .9 }
  to   { transform: scale(2.1); opacity: 0  }
}

/* ====================================================== 15/18 · scroll-driven ==
   Tied to scroll position rather than to a threshold, so it tracks the finger
   exactly and runs off the main thread — it stays smooth even while a directory
   is busy building itself. Replaces scroll listeners; does not add any.
   Chromium-only today: elsewhere both elements render in their resting state and
   nothing is lost. */
@supports (animation-timeline: scroll()) {
  .lc-hdr-condense {
    animation: lc-condense linear both;
    animation-timeline: scroll();
    animation-range: 0 120px;
  }
  @keyframes lc-condense { to { --lc-hdr-scale: .82 } }

  .lc-read-prog {
    position: fixed; inset-inline: 0; top: 0; z-index: 999; height: 2px;
    background: var(--lc-rose); transform-origin: 0 50%; transform: scaleX(0);
    animation: lc-fill linear both;
    animation-timeline: scroll();
  }
  @keyframes lc-fill { to { transform: scaleX(1) } }
}

/* ============================================================= 16 · buttons ==
   These sit on the paths that make money. The depth is real on press; the
   confirmation state is applied only when the server has actually answered. */
.lc-press {
  transition: transform .12s var(--lc-ease), box-shadow .12s var(--lc-ease), background .3s;
  box-shadow: 0 3px 0 var(--lc-btn-shadow, rgba(0,0,0,.28));
}
.lc-press:active { transform: translateY(3px); box-shadow: 0 0 0 var(--lc-btn-shadow, rgba(0,0,0,.28)); }
.lc-press[aria-busy="true"] { cursor: progress; opacity: .72; }

/* ============================================================== 17 · fields ==
   The label lifts instead of vanishing, so the question stays readable while it
   is being answered. Requires placeholder=" " on the input. Stays above 12px —
   shrunk past that it stops being the field's name and becomes decoration. */
.lc-field { position: relative; }
.lc-field > input,
.lc-field > textarea { padding-top: 19px; }
.lc-field > label {
  position: absolute; left: 12px; top: 14px; font-size: 15px;
  transform-origin: 0 0; pointer-events: none; color: var(--lc-muted, #726C6C);
  transition: transform .22s var(--lc-ease), color .22s var(--lc-ease);
}
.lc-field > :is(input, textarea):focus + label,
.lc-field > :is(input, textarea):not(:placeholder-shown) + label {
  transform: translateY(-9px) scale(.8);
}

/* ============================================================= 20 · the tide ==
   The one signature, and it stays special only by being scarce: native app cold
   start and a hard page boot. Two placements maximum. The moment it shows up on
   a filter click it is dead and should be deleted. */
.lc-tide { position: relative; height: 120px; }
.lc-tide > span { position: absolute; inset-inline: 0; height: 1px; background: currentColor; opacity: .18; }
.lc-tide > span:nth-child(1) { top: 52% }
.lc-tide > span:nth-child(2) { top: 58%; opacity: .1 }
.lc-tide::before {
  content: ''; position: absolute; inset-inline: -30%; top: calc(52% - 1px);
  height: 2px; border-radius: 99px;
  background: radial-gradient(closest-side, var(--lc-rose) 0%, transparent 100%);
  animation: lc-swell 9s var(--lc-ease) infinite;
}
@keyframes lc-swell {
  0%, 100% { transform: translateX(-14%) scaleY(1);   opacity: .25 }
  50%      { transform: translateX(14%)  scaleY(3.4); opacity: .9  }
}

/* ====================================================== reduced motion ====== */
/* Nothing here removes meaning: skeletons still hold their shape, labels still
   name what is loading, saved states still read as saved. Only the movement goes. */
@media (prefers-reduced-motion: reduce) {
  .lc-skel-card::after,
  .lc-pin-in, .lc-arrive > *, .lc-arrive-auto > *,
  .lc-photo-in, .lc-unfurl, .lc-sheet-in, .lc-bloom, .lc-bloom::after,
  .lc-think i, .lc-typing, .lc-roll.is-rolling,
  .lc-hdr-condense, .lc-read-prog, .lc-tide::before {
    animation: none !important;
    transition: none !important;
  }
  /* The roll's counter reads --lc-num, which the animation would normally carry
     to its end value. With the animation off it must already BE the end value or
     the count renders 0 — a silent wrong number is worse than no motion. */
  .lc-roll { --lc-num: var(--lc-to, 0); }
  /* The gate and the label's 1s threshold are NOT motion — they are information
     design, and they are the whole three-lengths rule. Killing them here would
     hand reduced-motion visitors a skeleton that flashes on every fast request,
     which is the exact defect specimen 04 exists to remove. So the fade goes and
     the TIMING stays: a hard cut at the same two thresholds.

     A 1ms LINEAR run, not steps() — with steps(1, end) the value the forwards
     fill settles on is ambiguous across engines, and in Chrome it left the label
     invisible permanently. 1ms is imperceptible and unambiguous. */
  .lc-pending > .lc-pending-veil {
    animation: lc-gate-in .001s linear var(--lc-gate) both !important;
  }
  .lc-pending-label {
    animation: lc-gate-in .001s linear 1s both !important;
  }
  .lc-pending-label::before { animation: none !important; }
  .lc-arrive > *, .lc-arrive-auto > *, .lc-pin-in, .lc-photo-in {
    opacity: 1; transform: none; filter: none;
  }
  .lc-sheet-in  { transform: translateY(0) }
  .lc-read-prog { display: none }
  .lc-btn:active { transform: none }
  ::view-transition-old(root), ::view-transition-new(root) { animation: none }
}
