/* Production copy of design/mobile-refresh/final/ambient.css, the owner-approved mockup (TABL Navy + ambient motion,
   2026-09-28), written by design/mobile-refresh/export.cjs. Per-page order: onlocation.styles.css, calculator.css (index,
   pricing), home.css (index) | pages.css (download, 404, privacy, eula), ambient.css (not on legal pages), tabl-navy.css last. */
/* TABL Mobile "On Location": ambient motion (mockup: final/ambient.css; production folds it into onlocation.styles.css).
   Owner, 2026-09-28: "add some ambient motion to the site so it doesn't look so flat". This overrides spec §4 "nothing loops".
   Decorative only: slow, low-amplitude loops on transform and opacity. Nothing that carries text, a button, a field, a price
   or the calculator ever moves. Notes, timings and palette tuning: research/motion-notes.md.

   ZERO IMPACT WHEN MOTION IS OFF. Every rule that paints or animates sits inside
   @media (prefers-reduced-motion: no-preference) and (forced-colors: none) and (prefers-contrast: no-preference)
   AND under :root[data-motion="on"], which only script sets (the page's <head> script, else ambient.js; the footer's
   Pause animations button flips it). So reduced motion, forced colours, increased contrast (where the base design hides its
   own rings), motion off and no JS all render the static design. Outside that scope the hook elements (.amb) are
   display:none and no pseudo-element exists. ambient.js pauses each moment off-screen ([data-ambient="pause"]) and in
   hidden tabs.

   Tokens. Palettes may set any of these on :root or on a section. Colour tokens are read at the point of use with a fallback,
   so night bands and palettes that re-colour a section keep the right ink:
     --ambient-tempo      1        duration multiplier (1.25 = everything 25% slower)
     --ambient-strength   1        opacity multiplier for the large layers (field, ripples, lanes)
     --ambient-bloom      block    the hero colour field; set to none for a flat-field palette
     --ambient-field      accent 22% + --ambient-field-2 (signal 14%): the hero colour field behind the phone
     --ambient-live       signal-dot/signal-night 50/50 mint: reads on light AND dark grounds, so the hero field
                          can be paper or a navy panel. Tap to Pay ripples (hero, /download), queue packets, #87 ring
     --ambient-ripple     --ambient-live  the hero and /download ripple only
     --ambient-ripple-night --signal-night Tap stage waves on night
     --ambient-route      --node-ring     the run-of-show light (the route-node colour, so night strips follow);
                          --ambient-route-edge (transparent) a keyline round it, the road's own colour
     --ambient-order      --ambient-live  queue packets and the #87 arrival ring (set it on .ticket to recolour the ring alone)
     --ambient-lane       --frost-2       close band lanes
     --ambient-lane-paint frost 34%       the rolling #product lane dashes (a palette with white lane paint sets --frost)
     --ambient-map        none            the hero route map; block for a palette whose hero phone stands on a navy field
     --ambient-map-route  --accent        its arterial; --ambient-map-street (frost-2 12%) the grid. The target and the
                          pings are --signal-night and the stops --tn-ice: the map only runs on navy */

:root { --ambient-tempo: 1; --ambient-strength: 1; --ambient-live: color-mix(in srgb, var(--signal-dot), var(--signal-night)); }
.amb { display: none; }
/* TABL Navy, folded in from the mockup's palette bridge (it keyed these on the html data-palette attribute).
   Every palette except Concrete is a flat-field palette whose header says "no glows", so the hero field stays off there.
   TABL Navy: the brighter night mint for the ripple on the navy field. (Its other motion tokens sit in tabl-navy.css.) */
:root { --ambient-bloom: none; }
:root { --ambient-ripple: var(--signal-night); }

/* The Pause / Play animations buttons: one in the footer legal row, and an early copy ambient.js puts after the skip link
   (a .skip-link, so it is off-screen until focused). Shown by ambient.js only when motion is possible; otherwise they stay
   [hidden], so those renders are untouched. They must stay visible while motion is off, so these are the only rules not
   gated on data-motion. */
@media (prefers-reduced-motion: no-preference) and (forced-colors: none) and (prefers-contrast: no-preference) {
  .footer-legal:has(> .motion-toggle:not([hidden])) { display: flow-root; }
  /* A 44px tap target; the -13px block margins keep its row box 18px, so the legal row and the label don't move. */
  .footer-legal > .motion-toggle {
    float: right; min-height: 44px; margin: -13px -2px -13px 16px; padding: 0 2px;
    border: 0; background: none; color: var(--frost-2); font: inherit; line-height: 44px;
    text-decoration: underline; text-underline-offset: 0.2em; cursor: pointer;
  }
  .footer-legal > .motion-toggle:hover { color: var(--frost); }
  .skip-link.motion-toggle { border: 0; cursor: pointer; }
}

@media (prefers-reduced-motion: no-preference) and (forced-colors: none) and (prefers-contrast: no-preference) {
  :root[data-motion="on"] {
    .amb { display: block; position: absolute; pointer-events: none; }
    .amb i { position: absolute; inset: 0; border-radius: var(--r-dot); will-change: transform, opacity; }

    /* Off-screen sections and hidden tabs hold still (ambient.js). */
    [data-ambient="pause"], [data-ambient="pause"]::before, [data-ambient="pause"]::after,
    [data-ambient="pause"] *, [data-ambient="pause"] *::before, [data-ambient="pause"] *::after,
    &[data-ambient-tab="hidden"] *, &[data-ambient-tab="hidden"] *::before, &[data-ambient-tab="hidden"] *::after {
      animation-play-state: paused !important;
    }

    /* 1. Hero colour field: two soft blobs orbit the phone once a minute, masked off the copy side like the rings. */
    .amb-field {
      display: var(--ambient-bloom, block); z-index: -2; left: 50%; top: 50%; width: 800px; height: 800px; margin: -400px 0 0 -400px; /* 800, not 1100: two big masked layers dropped frames */
      opacity: var(--ambient-strength);
      -webkit-mask: linear-gradient(90deg, transparent 18%, #000 50%); mask: linear-gradient(90deg, transparent 18%, #000 50%);
    }
    .amb-field i {
      border-radius: 0;
      background:
        radial-gradient(closest-side at 36% 40%, var(--ambient-field, color-mix(in srgb, var(--accent) 22%, transparent)), transparent),
        radial-gradient(closest-side at 66% 62%, var(--ambient-field-2, color-mix(in srgb, var(--signal-dot) 14%, transparent)), transparent);
      animation: amb-orbit calc(60s * var(--ambient-tempo)) linear infinite;
    }
    @media (max-width: 720px) { .amb-field { display: none; } } /* phones: the lighter set */

    /* 2. Tap to Pay ripple: three contactless rings, 2s apart, so one is always in flight (same centre and mask as
       .hero-rings). The negative delays start the loop mid-flight, so the field is never still, not even on load. */
    .amb-ripple { --d: 1000px; }
    .hero-device > .amb-ripple {
      z-index: -1; left: 50%; top: calc(var(--bz) + 84.5% * 0.94);
      -webkit-mask: linear-gradient(90deg, transparent 22%, #000 58%), radial-gradient(closest-side, #000 18%, transparent 100%);
      -webkit-mask-composite: source-in;
      mask: linear-gradient(90deg, transparent 22%, #000 58%) intersect, radial-gradient(closest-side, #000 18%, transparent 100%);
      /* Cut at the navy field's seam, like the map, so no ring ever reaches the paper side (the owner's clean straight
         edge): >1240 clamp(24px, 3vw, 42px) left of the phone, 961-1240 the phone's centre line. */
      clip-path: inset(0 0 0 calc(50% - var(--screen) / 2 - var(--bz) - clamp(24px, 3vw, 42px)));
    }
    @media (max-width: 1240px) { .hero-device > .amb-ripple { clip-path: inset(0 0 0 50%); } }
    @media (max-width: 960px) { .hero-device > .amb-ripple { clip-path: none; } }
    @media (max-width: 720px) {
      .hero-device > .amb-ripple {
        --d: 720px;
        -webkit-mask: linear-gradient(#000 42%, transparent 54%), radial-gradient(closest-side, #000 18%, transparent 100%);
        -webkit-mask-composite: source-in;
        mask: linear-gradient(#000 42%, transparent 54%) intersect, radial-gradient(closest-side, #000 18%, transparent 100%);
      }
    }
    .amb-ripple { width: var(--d); height: var(--d); margin: calc(var(--d) / -2) 0 0 calc(var(--d) / -2); opacity: var(--ambient-strength); }
    .amb-ripple i {
      border: 2px solid var(--ambient-ripple, var(--ambient-live)); opacity: 0;
      animation: amb-ripple calc(6s * var(--ambient-tempo)) cubic-bezier(0.25, 0.4, 0.55, 1) infinite;
    }
    .amb-ripple i:nth-child(2) { animation-delay: calc(-2s * var(--ambient-tempo)); }
    .amb-ripple i:nth-child(3) { animation-delay: calc(-4s * var(--ambient-tempo)); }
    /* TABL Navy drops the static rings, so no line crossed the callout copy there: a backing in the field's own navy lets
       the rings pass behind the words. An out-of-flow pseudo, so nothing moves; z-index -1 keeps it under the leader line;
       its left end fades in across the gap, so it never shows as an edge against the phone's shadow. */
    & .callout__label::before {
      content: ""; position: absolute; z-index: -1; inset: -8px -6px -2px calc(-1 * var(--gap));
      background: linear-gradient(90deg, transparent, var(--night) calc(var(--gap) + 32px));
    }
    & .callout--below .callout__label::before { inset: -2px -6px -8px calc(-1 * var(--gap)); }

    /* 2b. Hero route map (--ambient-map: block; TABL Navy). The phone stands on a live ops map: a faint street grid turned
       -22deg (so no street reads as a callout leader line), one TABL-blue arterial, small ice stops, and one traveller,
       a plain mint dot (not the live pin's target shape, so callout 3 alone owns that) that slides out from behind the
       phone, dwells at each stop as it pings, and drives off the edge. Every arrival lands on a whole even second, the
       frame a Tap ripple ring launches (the map and the ripple start and pause together). Flat lines and dots on
       transform/opacity only: no glow, no blur, no stroke animation.
       The box is 280% x 130% of the phone, so the moving parts are % of it and scale with --screen. It is cut at the
       field's own seam (>1240: clamp(24px, 3vw, 42px) left of the phone; 961-1240: the phone's centre line), so it never
       reaches the paper side, and the callout labels' navy backing reads as a map-label halo over it.
       Per width (geometry and keyframes generated by map5, see research/motion-notes.md):
         >960     the arterial starts under the phone (no stub at the seam); stops 2 and 3; 24s
         721-960  the phone is centred in open field: the arterial crosses it, and stop 1 on the left joins; 24s
         <=720    the target drives a top street instead, so the first screen moves beside the phone's status bar; 12s */
    .hero-device > .amb-map {
      --ambient-map-t: 24s; display: var(--ambient-map, none); z-index: -2; left: -30%; top: -15%; width: 280%; height: 130%;
      clip-path: inset(-20% -200% -20% calc(100% * 0.3 / 2.8 - clamp(24px, 3vw, 42px)));
      animation: amb-in calc(1.2s * var(--ambient-tempo)) ease-out both;
    }
    @media (max-width: 1240px) { .hero-device > .amb-map { clip-path: inset(-20% -200% -20% calc(100% * 0.8 / 2.8)); } }
    @media (max-width: 960px) { .hero-device > .amb-map { clip-path: none; } }
    /* The grid is drawn past the box (x -300..1240, y -160..760 units) so the field is covered at every width; the moving
       parts keep the box's own % geometry. */
    .amb-map svg {
      position: absolute; left: -27.012%; top: -9.185%; width: 193.662%; height: 119.16%; max-width: none; overflow: visible; fill: none;
    }
    .amb-map path { vector-effect: non-scaling-stroke; }
    .amb-map__st { stroke: var(--ambient-map-street, color-mix(in srgb, var(--frost-2) 12%, transparent)); stroke-width: 1px; }
    .amb-map__rt { stroke: var(--ambient-map-route, var(--accent)); stroke-width: 1.5px; stroke-linecap: round; }
    .amb-map .amb-map__dot {
      inset: auto; left: -5px; top: -5px; width: 100%; height: 100%; border-radius: 0; opacity: 0;
      background: radial-gradient(circle at 5px 5px, var(--signal-night) 0 3.5px, var(--night) 4px 4.75px, transparent 5.25px) no-repeat;
      animation: amb-map-dot calc(var(--ambient-map-t) * var(--ambient-tempo)) linear infinite;
    }
    /* Stops: small filled ice dots (the callout anchors keep the white ring), and a mint ping as the target lands. */
    .amb-map .amb-map__stop {
      inset: auto; width: 4px; height: 4px; margin: -2px 0 0 -2px; will-change: auto; background: var(--tn-ice, var(--frost-2));
    }
    .amb-map__stop::after {
      content: ""; position: absolute; inset: -10px; border: 1.5px solid var(--signal-night); border-radius: inherit; opacity: 0;
      animation: amb-map-ping calc(var(--ambient-map-t) * var(--ambient-tempo)) linear infinite;
      animation-delay: calc(var(--at) * 1s * var(--ambient-tempo));
      will-change: transform, opacity;
    }
    .amb-map .amb-map__rt--l, .amb-map .amb-map__rt--p, .amb-map .amb-map__stop--n, .amb-map .amb-map__stop--p { display: none; }
    @media (max-width: 960px) {
      .amb-map .amb-map__rt--l, .amb-map .amb-map__stop--n { display: block; }
      .amb-map .amb-map__dot { animation-name: amb-map-dot-tab; }
    }
    @media (max-width: 720px) {
      .hero-device > .amb-map { --ambient-map-t: 12s; }
      .amb-map svg { -webkit-mask: linear-gradient(#000 78%, transparent 85%); mask: linear-gradient(#000 78%, transparent 85%); } /* the grid fades out above the legend */
      .amb-map .amb-map__rt--w, .amb-map .amb-map__rt--l, .amb-map .amb-map__stop { display: none; }
      .amb-map .amb-map__rt--p, .amb-map .amb-map__stop--p { display: block; }
      .amb-map .amb-map__dot { animation-name: amb-map-dot-phone; }
      .amb-map__stop::after { animation-name: amb-map-ping-12; }
    }

    /* 3. Run of show: a light dwells on each stop, then drives to the next; 4s a stop, one 24s day. A crisp dot, no halo
       (TABL Navy: the map's own mint traveller with a navy keyline, so it reads on the white first stop too).
       Each stop owns the leg to the next ring (its own width/height + the gap), so it lands on the ring at every width. */
    .runsheet__stops li::after {
      content: ""; position: absolute; z-index: 1; left: -5px; top: -4px; width: calc(100% + 24px); height: 25px; opacity: 0;
      background: radial-gradient(circle at 12.5px 12.5px, var(--ambient-route, var(--node-ring)) 0 3.5px,
        var(--ambient-route-edge, transparent) 4px 4.75px, transparent 5.25px) no-repeat;
      animation: amb-leg-x calc(24s * var(--ambient-tempo)) linear infinite;
      will-change: transform, opacity; pointer-events: none;
    }
    .runsheet__stops li:nth-child(2)::after { animation-delay: calc(4s * var(--ambient-tempo)); }
    .runsheet__stops li:nth-child(3)::after { animation-delay: calc(8s * var(--ambient-tempo)); }
    .runsheet__stops li:nth-child(4)::after { animation-delay: calc(12s * var(--ambient-tempo)); }
    .runsheet__stops li:nth-child(5)::after { animation-delay: calc(16s * var(--ambient-tempo)); animation-name: amb-leg-last; }
    @media (max-width: 1100px) { .runsheet__stops li::after { width: calc(100% + 16px); } }
    @media (max-width: 960px) { .runsheet__stops li::after { top: 8px; width: 25px; height: 100%; animation-name: amb-leg-y; } }
    @media (max-width: 720px) { .runsheet__stops li::after { top: 4px; } }

    /* 4. #product: lane dashes roll toward the next stop; the Tap stage NFC glyph sends out contactless waves. */
    /* The hook stands in for the connector (same flex slot, 2px, -24px into the gap) so its dashes can roll on transform. */
    .route__step:not(:last-child) .route__node::after { display: none; }
    .route__node > .amb-lane { position: relative; flex: 1; height: 2px; margin-right: -24px; }
    .route__node > .amb-lane > i:first-child { overflow: clip; border-radius: 0; will-change: auto; }
    .route__node > .amb-lane b {
      position: absolute; inset: 0 0 0 -22px;
      background: repeating-linear-gradient(90deg, var(--ambient-lane-paint, color-mix(in srgb, var(--frost) 34%, transparent)) 0 12px, transparent 12px 22px);
      animation: amb-march calc(2.4s * var(--ambient-tempo)) linear infinite;
      will-change: transform;
    }
    @media (max-width: 960px) { .route__node > .amb-lane { display: none; } }
    @media (max-width: 720px) { .route__node > .amb-lane { display: block; margin-right: 0; } }
    /* One sale, from the line to the queue (12s): a mint lit dash rides lane 1 and lands on stop 2 as the Tap stage sends a
       wave (the waves run every 6s from t=0; #product pauses as one, so they stay in step), then rides lane 2 to stop 3.
       Each stop pings as it lands: mint on the live stop, white on the queue. The packet stops short of the ring, so it
       never covers the numeral. Only where the lanes show (>960, and each rail card at <=720). */
    .route__node > .amb-lane > i + i { /* a lit dash (14 x 3px) among the 12px lane dashes: "this lane segment lit up" */
      inset: auto; left: 0; top: -3px; width: calc(100% - 16px); height: 8px; border-radius: 0; opacity: 0;
      background: linear-gradient(var(--ambient-ripple-night, var(--signal-night)) 0 0) 0 2.5px / 14px 3px no-repeat;
      animation: amb-sale calc(12s * var(--ambient-tempo)) linear calc(4.2s * var(--ambient-tempo)) infinite;
    }
    .route__step:nth-child(2) .amb-lane > i + i { animation-delay: calc(7s * var(--ambient-tempo)); }
    @media (min-width: 961px), (max-width: 720px) {
      .route__step:not(:first-child) .route__num { position: relative; }
      .route__step:not(:first-child) .route__num::after {
        content: ""; position: absolute; inset: -1.5px; border-radius: inherit; opacity: 0;
        border: 1.5px solid var(--frost);
        animation: amb-stop-ping calc(12s * var(--ambient-tempo)) linear calc(8.8s * var(--ambient-tempo)) infinite;
        will-change: transform, opacity;
      }
      .route__step:not(:first-child) .route__num--live::after { border-color: var(--signal-night); animation-delay: calc(6s * var(--ambient-tempo)); }
    }
    .tap-glyph > .amb-waves {
      --d: 344px; left: 50%; top: 50%;
      -webkit-mask: radial-gradient(closest-side, #000 12%, transparent 92%); mask: radial-gradient(closest-side, #000 12%, transparent 92%);
    }
    .amb-waves { width: var(--d); height: var(--d); margin: calc(var(--d) / -2) 0 0 calc(var(--d) / -2); opacity: var(--ambient-strength); }
    .amb-waves i {
      border: 2px solid var(--ambient-ripple-night, var(--signal-night)); opacity: 0;
      animation: amb-wave calc(6s * var(--ambient-tempo)) cubic-bezier(0.2, 0.55, 0.3, 1) infinite;
    }
    .amb-waves i + i { animation-delay: calc(0.8s * var(--ambient-tempo)); }

    /* 5. Live dots breathe: a halo leaves the dot every 3.2s; the dot itself never dims (it keeps its 3:1). */
    .pill__dot { position: relative; }
    .pill__dot::after {
      content: ""; position: absolute; inset: 0; border-radius: inherit; background: inherit; opacity: 0;
      animation: amb-breathe calc(3.2s * var(--ambient-tempo)) cubic-bezier(0.2, 0.6, 0.3, 1) infinite;
      will-change: transform, opacity;
    }

    /* 6. One queue: every 8s an order drops down one source lane (Mobile, then QR, then Portal), a 2px ring lights 3px
       round #87 as it lands, and a packet runs on to the Kitchen Display. Phones hide the lanes, so this moment is
       desktop/tablet only. (TABL Navy: white on its TABL-blue plate, 6.21.) */
    @media (min-width: 721px) {
      .queue__lanes { overflow: clip; }
      .queue__lanes i { position: relative; }
      .queue__lanes i::after {
        content: ""; position: absolute; left: -3px; top: -28px; width: 8px; height: 100%; opacity: 0;
        background: radial-gradient(circle at 4px 24px, var(--ambient-order, var(--ambient-live)) 0 3.75px, transparent 4.25px) no-repeat;
        animation: amb-drop calc(24s * var(--ambient-tempo)) cubic-bezier(0.5, 0, 0.3, 1) infinite;
        will-change: transform, opacity;
      }
      .queue__lanes i:nth-child(2)::after { animation-delay: calc(8s * var(--ambient-tempo)); }
      .queue__lanes i:nth-child(3)::after { animation-delay: calc(16s * var(--ambient-tempo)); }
      .queue__lanes--one i::after { animation: amb-drop-one calc(8s * var(--ambient-tempo)) cubic-bezier(0.5, 0, 0.3, 1) infinite; }
      .ticket:not(.ticket--prev) { position: relative; isolation: isolate; }
      .ticket:not(.ticket--prev)::after {
        content: ""; position: absolute; z-index: -1; inset: -6px; border-radius: calc(var(--r-md) + 5px); opacity: 0;
        border: 2px solid var(--ambient-order, var(--ambient-live)); /* 3px clear of the ticket's edge */
        animation: amb-arrive calc(8s * var(--ambient-tempo)) ease-in-out infinite;
        will-change: opacity; pointer-events: none;
      }
    }

    /* 7. Close band: two faint lane markings stream past, like driving at night. Background layer only. */
    .final-cta { position: relative; isolation: isolate; }
    .final-cta::before {
      content: ""; position: absolute; z-index: -1; inset: 0 -72px 0 0; pointer-events: none;
      background:
        repeating-linear-gradient(90deg, var(--ambient-lane, var(--frost-2)) 0 36px, transparent 36px 72px) 0 44px / 100% 2px no-repeat,
        repeating-linear-gradient(90deg, var(--ambient-lane, var(--frost-2)) 0 36px, transparent 36px 72px) 0 calc(100% - 40px) / 100% 2px no-repeat;
      opacity: calc(0.22 * var(--ambient-strength));
      -webkit-mask: linear-gradient(90deg, transparent 25%, #000 70%); mask: linear-gradient(90deg, transparent 25%, #000 70%);
      animation: amb-lanes calc(4s * var(--ambient-tempo)) linear infinite;
      will-change: transform;
    }
    @media (max-width: 720px) { .final-cta::before { background-position: 0 32px, 0 calc(100% - 22px); } }

    /* /download: the same Tap ripple off the static rings, and the mini run of show (desktop; phones draw that road per stop). */
    .dl-media > .amb-ripple {
      --d: 800px; z-index: -1; left: auto; top: 50%; right: calc(clamp(0px, 3vw, 40px) + 130px); margin: calc(var(--d) / -2) calc(var(--d) / -2) 0 0;
      -webkit-mask: radial-gradient(closest-side, #000 30%, transparent 100%); mask: radial-gradient(closest-side, #000 30%, transparent 100%);
    }
    .dl-media > .amb-ripple i { animation-name: amb-ripple-dl; }
    @media (max-width: 1100px) { .dl-media > .amb-ripple { right: 130px; } }
    @media (max-width: 720px) { .dl-media > .amb-ripple { --d: 680px; right: 50%; } }
    @media (min-width: 721px) {
      .dl-route__stops li::after {
        content: ""; position: absolute; z-index: 1; left: -4px; top: -4px; width: calc(100% + 32px); height: 25px; opacity: 0;
        background: radial-gradient(circle at 12.5px 12.5px, var(--ambient-route, var(--node-ring)) 0 3.5px,
          var(--ambient-route-edge, transparent) 4px 4.75px, transparent 5.25px) no-repeat;
        animation: amb-leg-x calc(24s * var(--ambient-tempo)) linear infinite;
        will-change: transform, opacity; pointer-events: none;
      }
      .dl-route__stops li:nth-child(2)::after { animation-delay: calc(4s * var(--ambient-tempo)); }
      .dl-route__stops li:nth-child(3)::after { animation-delay: calc(8s * var(--ambient-tempo)); animation-name: amb-leg-last; }
    }

    /* /404: the stage's lane marking rolls behind the phone. */
    .nf-stage::before { left: -40px; animation: amb-march-40 calc(2.5s * var(--ambient-tempo)) linear infinite; will-change: transform; }
  }

  @keyframes amb-orbit { to { transform: rotate(1turn); } }
  @keyframes amb-in { from { opacity: 0; } }
  /* map-dot:start (generated by map5: eased into and out of each stop; % of the map box) */
  @keyframes amb-map-dot {
    0% { transform: translate(40.79%, 67.47%); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 0.3, 1); }
    16.67% { transform: translate(60.61%, 59.22%); opacity: 1; }
    22.92% { transform: translate(60.61%, 59.22%); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 0.3, 1); }
    33.33% { transform: translate(73.44%, 53.89%); opacity: 1; }
    41.67% { transform: translate(73.44%, 53.89%); opacity: 1; animation-timing-function: cubic-bezier(0.5, 0, 0.8, 0.6); }
    57.5% { opacity: 1; }
    62.5% { transform: translate(100%, 42.83%); opacity: 0; }
    62.51%, 100% { transform: translate(100%, 42.83%); opacity: 0; }
  }
  @keyframes amb-map-dot-tab {
    0% { transform: translate(40.79%, 67.47%); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 0.3, 1); }
    16.67% { transform: translate(60.61%, 59.22%); opacity: 1; }
    22.92% { transform: translate(60.61%, 59.22%); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 0.3, 1); }
    33.33% { transform: translate(73.44%, 53.89%); opacity: 1; }
    41.67% { transform: translate(73.44%, 53.89%); opacity: 1; animation-timing-function: cubic-bezier(0.5, 0, 0.8, 0.6); }
    57.5% { opacity: 1; }
    62.5% { transform: translate(100%, 42.83%); opacity: 0; }
    64.58% { transform: translate(0%, 84.45%); opacity: 0; animation-timing-function: cubic-bezier(0.25, 0.6, 0.35, 1); }
    68.75% { opacity: 1; }
    75% { transform: translate(8.7%, 80.83%); opacity: 1; }
    83.33% { transform: translate(8.7%, 80.83%); opacity: 1; animation-timing-function: cubic-bezier(0.5, 0, 0.8, 0.6); }
    91.67% { transform: translate(40.79%, 67.47%); opacity: 1; }
    100% { transform: translate(40.79%, 67.47%); opacity: 1; }
  }
  @keyframes amb-map-dot-phone {
    0% { transform: translate(-5.63%, 39.3%); opacity: 0; }
    4.17% { opacity: 1; }
    20.83% { transform: translate(10.71%, 32.49%); opacity: 1; }
    37.5% { transform: translate(45.3%, 18.1%); opacity: 1; animation-timing-function: cubic-bezier(0.25, 0.6, 0.35, 1); }
    50% { transform: translate(52.84%, 14.96%); opacity: 1; }
    66.67% { transform: translate(52.84%, 14.96%); opacity: 1; animation-timing-function: cubic-bezier(0.5, 0, 0.8, 0.6); }
    81.67% { opacity: 1; }
    87.5% { transform: translate(64.79%, 9.99%); opacity: 0; }
    87.51%, 100% { transform: translate(64.79%, 9.99%); opacity: 0; }
  }
  /* map-dot:end */
  /* A stop pings as the dot arrives (--at: the arrival, in seconds into the loop): about 1s, on the 24s and 12s loops. */
  @keyframes amb-map-ping {
    0% { transform: scale(0.5); opacity: 0.9; }
    4.17%, 100% { transform: scale(1.6); opacity: 0; }
  }
  @keyframes amb-map-ping-12 {
    0% { transform: scale(0.5); opacity: 0.9; }
    8.33%, 100% { transform: scale(1.6); opacity: 0; }
  }
  @keyframes amb-ripple {
    0% { transform: scale(0.12); opacity: 0; }
    10% { opacity: 1; }
    100% { transform: scale(1); opacity: 0; }
  }
  @keyframes amb-ripple-dl {
    0% { transform: scale(0.3); opacity: 0; }
    10% { opacity: 1; }
    100% { transform: scale(1); opacity: 0; }
  }
  @keyframes amb-wave {
    0% { transform: scale(0.14); opacity: 0; }
    8% { opacity: 1; }
    75%, 100% { transform: scale(1); opacity: 0; }
  }
  /* One leg = 16.667% of the 24s day: fade in, dwell 1.5s on the ring, ease 2.5s to the next ring, hand over. */
  @keyframes amb-leg-x {
    0% { transform: none; opacity: 0; }
    0.8% { opacity: 1; }
    6.25% { transform: none; animation-timing-function: cubic-bezier(0.45, 0, 0.25, 1); }
    16.667% { transform: translateX(100%); opacity: 1; }
    17.5%, 100% { transform: translateX(100%); opacity: 0; }
  }
  @keyframes amb-leg-y {
    0% { transform: none; opacity: 0; }
    0.8% { opacity: 1; }
    6.25% { transform: none; animation-timing-function: cubic-bezier(0.45, 0, 0.25, 1); }
    16.667% { transform: translateY(100%); opacity: 1; }
    17.5%, 100% { transform: translateY(100%); opacity: 0; }
  }
  /* The last stop (Close out): dwell, then fade; the next day starts 10s later. */
  @keyframes amb-leg-last {
    0% { opacity: 0; }
    0.8%, 8% { opacity: 1; }
    12%, 100% { opacity: 0; }
  }
  @keyframes amb-march { to { transform: translateX(22px); } }
  /* A packet crosses its lane in 1.8s (ease in, ease out), lands, and fades as the stop pings. */
  @keyframes amb-sale {
    0% { transform: none; opacity: 0; animation-timing-function: cubic-bezier(0.45, 0, 0.3, 1); }
    2% { opacity: 1; }
    15% { transform: translateX(100%); opacity: 1; }
    17.5%, 100% { transform: translateX(100%); opacity: 0; }
  }
  @keyframes amb-stop-ping { /* from 1.15, just outside the static ring, so the ping shows on the landing frame */
    0% { transform: scale(1.15); opacity: 0.9; }
    12%, 100% { transform: scale(1.9); opacity: 0; }
  }
  @keyframes amb-march-40 { to { transform: translateX(40px); } }
  @keyframes amb-breathe {
    0% { transform: scale(1); opacity: 0.5; }
    70%, 100% { transform: scale(2.8); opacity: 0; }
  }
  /* A packet crosses its lane in 1.4s of its 24s turn (each lane fires once every 24s, 8s apart). */
  @keyframes amb-drop {
    0% { transform: none; opacity: 0; }
    0.5% { opacity: 1; }
    6% { transform: translateY(100%); opacity: 1; }
    6.5%, 100% { transform: translateY(100%); opacity: 0; }
  }
  /* #87 lights as the packet lands (1.4s), holds, fades; the kitchen packet leaves at 3.4s. */
  @keyframes amb-arrive {
    0%, 17% { opacity: 0; }
    25%, 40% { opacity: 1; }
    65%, 100% { opacity: 0; }
  }
  @keyframes amb-drop-one {
    0%, 42% { transform: none; opacity: 0; }
    43.5% { opacity: 1; }
    58% { transform: translateY(100%); opacity: 1; }
    60%, 100% { transform: translateY(100%); opacity: 0; }
  }
  @keyframes amb-lanes { to { transform: translateX(-72px); } }
}
