/* ==========================================================================
   MOTION  ·  SPEC 9. Only transform and opacity move. One curve (--hl-ease-out), nothing overshoots, nothing scales past 1.04, one looping light per screen (today's door).
   Times: 90 ms a press, 160 ms a state, 240 ms a toast or the postmark, 280 ms the tab indicator, 320 ms a sheet or a push, 600 ms a light changing.
   EVERY animation and transition is declared inside @media (prefers-reduced-motion: no-preference): under `reduce` nothing moves (today's door rests at .8, a screen change is a swap, nothing shimmers or shakes),
   except the ring that turns while something loads: a state, not decoration. State is never colour alone, so nothing is lost. A screen's own CSS follows the same rule (tests/app/ui/layout.test.mjs looks at
   every running animation, and at every transition, and refuses a box-shadow or a background-position that moves).
   Fifteen @keyframes in all: here (spill, rise, postmark-land, spin, shimmer, shake, tick, dialog-out, toast-in, toast-out, fade-up, banner-in), in shell.css (screen-in, screen-out) and in index.html (splash-fade).
   ========================================================================== */
@keyframes hl-spin { to { transform: rotate(1turn); } }
/* the shimmer of a placeholder: a soft band of light slides across it. The band is a layer (a pseudo-element) moved with transform; the placeholder itself keeps one flat colour */
@keyframes hl-shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
/* the one glow of a screen: the spill behind today's door breathes between .65 and .9 (it rests at .8) */
@keyframes hl-spill { 0%, 100% { opacity: .65; } 50% { opacity: .9; } }
/* a result arrives: the slip rises 12 px with opacity; the letter rises 8 px (--hl-rise) */
@keyframes hl-rise { from { opacity: 0; transform: translateY(var(--hl-rise, 12px)); } to { opacity: 1; transform: none; } }
/* the postmark lands: transform and opacity on the svg root (the individual `scale` property on an SVG root measured 362 main-thread frames against 0 paints) */
@keyframes hl-postmark-land { from { opacity: 0; transform: rotate(-9deg) scale(1.04); } to { opacity: .85; transform: rotate(-9deg) scale(1); } }
/* number tick: .hl-ticker.is-ticking (ui/number-ticker.js) plays this when the value changes */
@keyframes hl-tick { from { transform: translateY(.45em); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes hl-dialog-out { to { opacity: 0; transform: translateY(8px); } }
@keyframes hl-toast-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
@keyframes hl-toast-out { to { opacity: 0; transform: translateY(-8px); } }
@keyframes hl-shake { 0%, 100% { transform: none; } 25% { transform: translateX(-4px); } 50% { transform: translateX(4px); } 75% { transform: translateX(-2px); } }
@keyframes hl-fade-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes hl-banner-in { from { opacity: 0; transform: translateY(-100%); } to { opacity: 1; transform: none; } }

/* a ring that turns says "busy": state, not decoration, so it runs under reduced motion too */
.hl-btn.is-loading::after, .hl-ticket.is-loading .hl-play::after, .hl-spinner { animation: hl-spin .8s linear infinite; }

@media (prefers-reduced-motion: no-preference) {
  /* ---- the look */
  .hl-day--today::before { animation: hl-spill 4s ease-in-out infinite; }
  .hl-paper.is-arriving { animation: hl-rise var(--hl-dur-sheet) var(--hl-ease-out) both; }
  .hl-letter.is-arriving { --hl-rise: 8px; }
  .hl-paper.is-arriving .hl-postmark { animation: hl-postmark-land var(--hl-dur-chip) var(--hl-ease-out) 180ms both; }

  /* ---- press: scale .97 in 90 ms, back out in 200 ms (the ticket .985); a colour change is instant */
  :is(.hl-btn, .hl-chip, a.hl-door, .hl-send, .hl-avlink, .hl-appbar__back, .hl-msg__more, .hl-card--action, .hl-row--action, .hl-opt, .hl-avopt, .hl-seg__item, .hl-link-ink, .hl-msg__retry) { transition: transform 200ms var(--hl-ease-out); }
  :is(.hl-btn, .hl-chip, a.hl-door, .hl-send, .hl-avlink, .hl-appbar__back, .hl-msg__more, .hl-card--action, .hl-row--action, .hl-opt, .hl-avopt, .hl-seg__item):is(:active, .is-pressed):not(:disabled, [aria-disabled="true"], .is-disabled, .is-loading) { transform: scale(.97); transition-duration: var(--hl-dur-tap); }
  .hl-ticket-wrap { transition: transform 200ms var(--hl-ease-out); }
  .hl-ticket-wrap:has(.hl-ticket:is(:active, .is-pressed):not([aria-disabled="true"], .is-loading)), .hl-ticket-wrap.is-pressed { transform: scale(.985); transition-duration: var(--hl-dur-tap); }
  .hl-toggle__knob { transition: transform var(--hl-dur-fast) var(--hl-ease-out); }

  /* ---- tabs: the 24 x 2 indicator moves by transform (a compositor layer; margin-left laid the page out at every frame); the icon and label change colour in 160 ms */
  .hl-tabbar__glide { transition: transform var(--hl-dur-tab) var(--hl-ease-out), opacity var(--hl-dur-fast) var(--hl-ease-out); }
  .hl-tab { transition: color var(--hl-dur-fast) var(--hl-ease-out); }

  /* ---- the Star meter: the arc does not draw (stroke-dasharray is not transform or opacity): the old ring fades out and the new fades in while the lit Star's opacity follows */
  .hl-meter__star-lit, .hl-meter__arc { transition: opacity var(--hl-dur-celebrate) var(--hl-ease-out); }
  .hl-meter__arc.is-leaving { opacity: 0; }

  /* ---- a door opens at midnight: its lit glass fades in over 600 ms (ui/day-street.js animates the layer; nothing under reduced motion) */

  /* ---- sheets, dialogs, toasts, the scrim and the rest of the overlays */
  .hl-scrim { transition: opacity var(--hl-dur-sheet) var(--hl-ease-out); }
  .hl-sheet { transition: transform var(--hl-dur-sheet) var(--hl-ease-out), visibility 0s linear var(--hl-dur-sheet); }
  .hl-sheet.is-open { transition-delay: 0s; }
  .hl-sheet.is-dragging { transition: none; }
  .hl-sheet.is-settling { transition: transform 240ms var(--hl-ease-out); }
  .hl-dialog { animation: hl-fade-up var(--hl-dur-sheet) var(--hl-ease-out) both; }
  .hl-dialog.is-leaving { animation: hl-dialog-out var(--hl-dur-fast) var(--hl-ease-out) both; }
  .hl-toast { animation: hl-toast-in var(--hl-dur-chip) var(--hl-ease-out) both; }
  .hl-toast.is-leaving { animation: hl-toast-out var(--hl-dur-fast) var(--hl-ease-out) both; }
  .hl-banner { animation: hl-banner-in var(--hl-dur-sheet) var(--hl-ease-out) both; }
  .hl-lightbox { transition: opacity var(--hl-dur-sheet) var(--hl-ease-out); }
  .hl-ptr.is-settling { transition: transform 260ms var(--hl-ease-out), opacity 200ms var(--hl-ease-out); }
  .hl-ptr.is-refreshing .hl-ptr__star { animation: hl-spin 1.2s linear infinite; }
  .hl-ticker.is-ticking { animation: hl-tick var(--hl-dur-sheet) var(--hl-ease-out) both; }

  /* ---- state, not decoration, but still movement: kept and restyled with these tokens */
  .hl-skel::after, .hl-chip.is-loading::before, .hl-avatar.is-loading::after, .hl-row.is-loading::after, .hl-opt.is-loading::after, .hl-story.is-loading .hl-plate::after { animation: hl-shimmer 1.4s linear infinite; }
  .hl-btn.is-error, .hl-field.is-error:not(.hl-field--pin) :is(.hl-input, .hl-textarea), .hl-field--pin.is-error .hl-pin, .hl-opt.is-wrong, .hl-ticket-wrap.is-error .hl-ticket { animation: hl-shake .4s var(--hl-ease-out) 1; }
}
