atris 3.74.7 → 3.74.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,8 +13,12 @@
13
13
  // then settles small at the top and the card fades up. Tap again and the
14
14
  // card dissolves, the lava melts back into the sphere, and the next bet plays.
15
15
  // Bets wait in a queue on the Mac, so a tap rarely waits; when it does, the
16
- // spiral follows the real progress. Once a card is up nothing moves. The
17
- // footer is only the count of jobs the team is working on.
16
+ // spiral follows the real progress. Once a card is up nothing moves, but
17
+ // while the next card is on its way the dots never stop: after an answer
18
+ // they swarm in the sphere until it lands, and on the calm line (writing
19
+ // your next idea) the sphere stays above the card, swarming, its box open
20
+ // for a note, until the idea grows out of the dots. The footer is only the
21
+ // count of jobs the team is working on.
18
22
  //
19
23
  // Every card is big type in two parts: what is going on, and why it matters
20
24
  // to the business. Nothing about how work is run or checked reaches the page.
@@ -70,7 +74,10 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
70
74
  }
71
75
  .line.out { opacity: 0; }
72
76
  .line.bad { color: var(--text-2); }
73
- .foot { position: absolute; left: 0; right: 0; bottom: max(20px, env(safe-area-inset-bottom)); text-align: center; }
77
+ .foot { position: absolute; left: 0; right: 0; bottom: max(20px, env(safe-area-inset-bottom)); text-align: center; transition: opacity 0.3s ease-out 0.1s; }
78
+ /* while a card moves the footer steps aside, so it never jumps between the
79
+ bottom of the screen and the end of a card; it comes back at rest */
80
+ body[data-busy]:not([data-in-place]) .foot { opacity: 0; transition: opacity 0.15s ease-in; }
74
81
  /* the yeses still waiting: a small line beside the team, never a card on its own */
75
82
  #needs { margin-right: 20px; color: var(--amber); }
76
83
  .quiet { color: var(--text-2); font-size: 17px; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 44px; }
@@ -150,7 +157,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
150
157
  .bet.quick { transition-duration: 0.4s, 0.5s; }
151
158
  /* after an act the next card fades up as the sphere settles, so the whole motion is about a
152
159
  second, and its title drifts only a few pixels into the spot the last title had */
153
- .bet.acted { transform: translateY(4px); transition-duration: 0.28s, 0.36s; }
160
+ .bet.acted { transform: translateY(4px); transition-duration: 0.3s, 0.38s; }
154
161
  .bet.acted.up { transform: none; }
155
162
  /* card to card: a crossfade in place. the old card is a still copy that
156
163
  fades over the box while the new card fades in under it from a small
@@ -201,11 +208,37 @@ body.has-card .foot { position: static; margin-top: 28px; }
201
208
  .bet h1.said { font-size: 20px; line-height: 1.4; font-weight: 400; letter-spacing: 0; }
202
209
  .bet.holding { pointer-events: none; }
203
210
  .muted { opacity: 0.35; transition: opacity 0.2s ease; }
211
+ /* after an act the card fades all the way before it leaves the page, never cut mid fade */
212
+ .bet.leave.fast { transition: opacity 0.2s ease-in-out; }
213
+
214
+ /* ---- the calm line: the sphere stays, a little smaller and higher, its dots
215
+ moving the whole time. The card under it takes no room of its own, so the
216
+ sphere never jumps when the card comes or goes; it glides. ---- */
217
+ .stage { transform-origin: 50% 0; transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1); }
218
+ body.has-card.waiting .screen { justify-content: center; padding-top: max(24px, env(safe-area-inset-top)); }
219
+ body.has-card.waiting .stage { display: block; transform: translateY(var(--wait-dy, 0px)) scale(var(--wait-k, 1)); }
220
+ body.has-card.waiting .line { display: block; }
221
+ body.has-card.waiting .foot { position: absolute; margin-top: 0; }
222
+ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; top: var(--wait-top, 62%); padding-bottom: 76px; }
223
+ .bet.wait-in { transform: translateY(8px); transition: opacity 0.4s ease-out 0.28s, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.28s; }
224
+ .bet.wait-in.up { transform: none; }
225
+ .bet[data-kind="wait"] h1 { text-align: center; }
226
+ .waitstep { margin: 10px 0 0; min-height: 1.5em; text-align: center; color: var(--text-2); font-size: 17px; transition: opacity 0.26s ease; }
227
+ .waitstep.out { opacity: 0; }
228
+ .waittime { display: block; min-height: 1.5em; text-align: center; color: var(--text-3); font-size: 17px; }
229
+ .waitnotes { margin: 6px 0 0; overflow: hidden; transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
230
+ .waitnotes .turn { text-align: center; transition: opacity 0.3s ease-out; }
231
+ .waitnotes .turn.fresh { opacity: 0; }
232
+ .field.stay { height: 52px; }
204
233
 
205
234
  @media (prefers-reduced-motion: reduce) {
206
235
  .lava .tx, .lava .ty, .lava .blob { animation: none !important; }
207
236
  .bet { transform: none; transition: opacity 0.2s linear; }
208
237
  .line { transition: none; }
238
+ /* no travel and no swarm: every card change is a plain fade */
239
+ .stage, .waitstep, .waitnotes, .waitnotes .turn { transition: none; }
240
+ .bet.quick, .bet.acted, .bet.leave, .bet.leave.fast, .bet.wait-in { transform: none; transition: opacity 0.2s linear; }
241
+ .lava { transition: opacity 0.2s linear; }
209
242
  }
210
243
  </style>
211
244
  </head>
@@ -275,7 +308,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
275
308
  setTimeout(function () { busy = false; next(); }, 650);
276
309
  }, 280);
277
310
  }
278
- return { set: function (text, bad) { want = { text: text, bad: Boolean(bad) }; next(); } };
311
+ return {
312
+ set: function (text, bad) { want = { text: text, bad: Boolean(bad) }; next(); },
313
+ // the page just opened: no fade to wait out, so the next line comes at once
314
+ clear: function () { if (busy) { line.set('', false); return; } want = null; node.textContent = ''; node.classList.remove('bad', 'out'); },
315
+ };
279
316
  })();
280
317
 
281
318
  // ---- the orb: StippleOrb (tone dark) and LetterOrb's tap, ported 1:1 -----------
@@ -305,10 +342,24 @@ body.has-card .foot { position: static; margin-top: 28px; }
305
342
  // window side and never washes the page
306
343
  var FAST_GROW_MS = 460, FAST_SETTLE_MS = 440, BLOOM = 0.72;
307
344
  // the owner acted on a card: one motion of about a second when the answer
308
- // is in. The card fades while the small orb glides into the sphere, the
309
- // dots spiral in at ACT_PACE, and the next card blooms where they land.
310
- var ACT_LEAVE_MS = 160, ACT_GLIDE_MS = 180, ACT_BEAT_MS = 100, ACT_PACE = 0.24, ACT_GROW_MS = 180, ACT_SETTLE_MS = 260;
345
+ // is in. The card fades all the way while the small orb glides into the
346
+ // sphere, the dots spiral in at ACT_PACE, and the next card grows out of
347
+ // the point: the lava swells to ACT_BLOOM and is already drifting up as
348
+ // it does, so its size and place never stop or snap on the way.
349
+ var ACT_LEAVE_MS = 220, ACT_GLIDE_MS = 260, ACT_BEAT_MS = 100, ACT_PACE = 0.28, ACT_GROW_MS = 140, ACT_SETTLE_MS = 420, ACT_BLOOM = 0.56;
311
350
  var quick = false;
351
+ // the swarm: while the next card is on its way the dots never stop. The
352
+ // sphere turns at SWARM_SPIN and every dot wanders a slow loop of its
353
+ // own, WANDER of the radius, springing after it. In and out of it the
354
+ // motion eases over about a second, and it ends the moment a card lands.
355
+ var IDLE_SPIN = 0.15, SWARM_SPIN = 0.5, WANDER = 0.1, SWARM_EASE = 0.04;
356
+ var swarmK = 0, spin = IDLE_SPIN, clock = 0;
357
+ // when a spiral starts, each dot keeps the speed it had and lets it fade
358
+ // at CARRY a frame, so the dots turn inward without a stop
359
+ var CARRY = 0.82;
360
+ // what the dots did on the last frame, kept on the canvas so the motion can be measured
361
+ var stats = { frame: 0, mode: 'idle', step: 0, mean: 0 };
362
+ canvas.__orb = stats;
312
363
  // Two speeds, chosen by truth. A bet already waiting in the queue plays
313
364
  // the same curves at FAST of their length (tap to card in about two seconds, unhurried).
314
365
  // A bet still being made plays the full landing length, and the spiral
@@ -360,7 +411,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
360
411
  for (var i = 0; i < count; i++) {
361
412
  var gx = gauss(), gy = gauss(), gz = gauss();
362
413
  var n = Math.hypot(gx, gy, gz) || 1;
363
- dots.push({ x: gx / n, y: gy / n, z: gz / n, sx: cx, sy: cy, vx: 0, vy: 0, r: 0.4 + Math.random() * 0.7, bx: 0, by: 0, dl: 0 });
414
+ dots.push({
415
+ x: gx / n, y: gy / n, z: gz / n, sx: cx, sy: cy, vx: 0, vy: 0, r: 0.4 + Math.random() * 0.7, bx: 0, by: 0, dl: 0, kx: 0, ky: 0,
416
+ // its own slow loop in the swarm
417
+ f1: 0.5 + Math.random() * 0.8, f2: 0.5 + Math.random() * 0.8, p1: Math.random() * 6.283, p2: Math.random() * 6.283,
418
+ });
364
419
  }
365
420
  }
366
421
 
@@ -372,7 +427,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
372
427
  var cosA = Math.cos(angle), sinA = Math.sin(angle);
373
428
  for (var i = 0; i < dots.length; i++) {
374
429
  var d = dots[i];
375
- if (fromHere) { d.bx = d.sx; d.by = d.sy; } else { d.bx = cx + (d.x * cosA + d.z * sinA) * R; d.by = cy + d.y * R; }
430
+ if (fromHere) { d.bx = d.sx; d.by = d.sy; } else { d.bx = cx + (d.x * cosA + d.z * sinA) * R; d.by = cy + d.y * R; d.vx = 0; d.vy = 0; }
431
+ d.kx = 0; d.ky = 0;
376
432
  var len = Math.hypot(d.bx - cx, d.by - cy);
377
433
  d.dl = (1 - Math.min(1, len / R)) * 0.28 + Math.random() * 0.05;
378
434
  }
@@ -384,7 +440,12 @@ body.has-card .foot { position: static; margin-top: 28px; }
384
440
  var dt = Math.min(0.05, (now - (lastT || now)) / 1000);
385
441
  lastT = now;
386
442
  ctx.clearRect(0, 0, w, h);
387
- if (!reduced && mode === 'idle') angle += dt * 0.15;
443
+ var swarming = mode === 'swarm' && !reduced;
444
+ var toward = 1 - Math.pow(1 - SWARM_EASE, dt * 60);
445
+ swarmK += ((swarming ? 1 : 0) - swarmK) * toward;
446
+ spin += ((swarming ? SWARM_SPIN : IDLE_SPIN) - spin) * toward;
447
+ clock += dt;
448
+ if (!reduced && (mode === 'idle' || mode === 'swarm')) angle += dt * spin;
388
449
  wave.age += dt;
389
450
  light.x += (light.tx - light.x) * 0.06;
390
451
  light.y += (light.ty - light.y) * 0.06;
@@ -400,13 +461,19 @@ body.has-card .foot { position: static; margin-top: 28px; }
400
461
  else age += (ageTo - age) * 0.05;
401
462
  }
402
463
  var cosA = Math.cos(angle), sinA = Math.sin(angle);
464
+ var most = 0, sum = 0, wander = WANDER * R * swarmK;
403
465
  for (var i = 0; i < dots.length; i++) {
404
466
  var d = dots[i];
405
467
  var fade = 1, shrink = 1;
468
+ var x0 = d.sx, y0 = d.sy;
406
469
  var rx = d.x * cosA + d.z * sinA;
407
470
  var rz = -d.x * sinA + d.z * cosA;
408
- if (mode === 'idle') {
471
+ if (mode === 'idle' || mode === 'swarm') {
409
472
  var homeX = cx + rx * R, homeY = cy + d.y * R;
473
+ if (wander > 0.05) {
474
+ homeX += Math.sin(clock * d.f1 + d.p1) * wander;
475
+ homeY += Math.sin(clock * d.f2 + d.p2) * wander;
476
+ }
410
477
  if (waveOn) {
411
478
  var wx = homeX - wave.x, wy = homeY - wave.y;
412
479
  var wd = Math.sqrt(wx * wx + wy * wy) || 1;
@@ -437,11 +504,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
437
504
  var e = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
438
505
  var ox = (d.bx - cx) * (1 - e), oy = (d.by - cy) * (1 - e);
439
506
  var turn = e * 1.5;
440
- d.sx = cx + ox * Math.cos(turn) - oy * Math.sin(turn);
441
- d.sy = cy + ox * Math.sin(turn) + oy * Math.cos(turn);
507
+ // the speed it had carries on and fades, then the spiral has it
508
+ d.kx += d.vx; d.ky += d.vy; d.vx *= CARRY; d.vy *= CARRY;
509
+ d.sx = cx + ox * Math.cos(turn) - oy * Math.sin(turn) + d.kx * (1 - e);
510
+ d.sy = cy + ox * Math.sin(turn) + oy * Math.cos(turn) + d.ky * (1 - e);
442
511
  fade = 1 - Math.pow(Math.max(0, (e - 0.8) / 0.2), 1.5);
443
512
  shrink = 1 - 0.35 * e;
444
513
  }
514
+ if (i < 64) { var stepped = Math.max(Math.abs(d.sx - x0), Math.abs(d.sy - y0)); if (stepped > most) most = stepped; sum += stepped; }
445
515
  // front dots are lit; the back half is a dim ghost. amber in shadow, cream where lit.
446
516
  var lit = rx * light.x + d.y * light.y + rz * lz;
447
517
  var front = rz > 0;
@@ -453,6 +523,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
453
523
  ctx.arc(d.sx, d.sy, d.r * (front ? 0.6 + l * 0.7 : 0.6) * shrink, 0, Math.PI * 2);
454
524
  ctx.fill();
455
525
  }
526
+ stats.frame += 1; stats.mode = mode; stats.step = most; stats.mean = sum / Math.min(64, dots.length || 1);
456
527
  if (mode === 'pull') {
457
528
  // where the dots land: one bright point that swells until the lava is born
458
529
  var arrived = Math.min(1, Math.max(0, (age - 0.3) / 0.7));
@@ -463,7 +534,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
463
534
  var left = 1 - (now - bridgeAt) / BRIDGE_MS;
464
535
  if (left > 0) point(left, 1); else bridgeAt = 0;
465
536
  }
466
- if (mode === 'card' || (reduced && mode === 'idle')) { raf = 0; return; }
537
+ // still once a card is up, and under reduced motion the sphere is drawn once and holds
538
+ if (mode === 'card' || (reduced && (mode === 'idle' || mode === 'swarm'))) { raf = 0; return; }
467
539
  raf = requestAnimationFrame(frame);
468
540
  }
469
541
 
@@ -496,13 +568,17 @@ body.has-card .foot { position: static; margin-top: 28px; }
496
568
  // A queued bet (the fast pace) blooms contained: the lava grows to BLOOM
497
569
  // of the smaller window side, the page never dims, and it settles.
498
570
  function grow() {
571
+ // fixed while it grows past the screen, so the page never scrolls sideways
572
+ window.scrollTo(0, 0);
499
573
  var rect = canvas.getBoundingClientRect();
500
574
  var contained = pace < 1;
501
575
  var growMs = quick ? ACT_GROW_MS : contained ? FAST_GROW_MS : (GROW_MS * pace);
502
576
  var settleMs = quick ? ACT_SETTLE_MS : contained ? FAST_SETTLE_MS : (SETTLE_MS * pace);
503
- var vx = rect.left + cx, vy = rect.top + cy;
577
+ // the point the dots drew, where the sphere is on screen (smaller and higher on the calm line)
578
+ var vx = rect.left + rect.width / 2, vy = rect.top + rect.height / 2;
579
+ var bloom = quick ? ACT_BLOOM : BLOOM;
504
580
  var reach = contained
505
- ? BLOOM * Math.min(window.innerWidth, window.innerHeight) / 2
581
+ ? bloom * Math.min(window.innerWidth, window.innerHeight) / 2
506
582
  : Math.hypot(Math.max(vx, window.innerWidth - vx), Math.max(vy, window.innerHeight - vy)) * 1.08;
507
583
  base = Math.round(Math.min(1000, Math.max(280, (2 * reach) / MAX_SCALE)));
508
584
  var sMax = (2 * reach) / base;
@@ -510,14 +586,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
510
586
  var rest = REST / base;
511
587
  lava.style.width = base + 'px';
512
588
  lava.style.height = base + 'px';
513
- // fixed while it grows past the screen, so the page never scrolls sideways
514
- window.scrollTo(0, 0);
515
589
  origin = { x: vx, y: vy };
516
590
  lava.style.position = 'fixed';
591
+ lava.style.opacity = '0';
592
+ place(sMin, origin.x, origin.y);
517
593
  lava.style.visibility = 'visible';
518
594
  lava.classList.remove('still', 'alive', 'tap');
519
595
  void lava.offsetWidth;
520
596
  lava.classList.add('alive');
597
+ if (contained) { rise(growMs, settleMs, sMin, sMax, rest); return; }
521
598
  var t0 = performance.now();
522
599
  var step = function (now) {
523
600
  var t = Math.min(1, (now - t0) / growMs);
@@ -545,6 +622,38 @@ body.has-card .foot { position: static; margin-top: 28px; }
545
622
  requestAnimationFrame(step);
546
623
  }
547
624
 
625
+ // A contained bloom is one motion. The lava is born at the point and
626
+ // swells for growMs on a soft curve while it already drifts up, then
627
+ // eases down into the small orb at the top over settleMs. Its size turns at the peak with no
628
+ // stop, and its place moves on one curve the whole way, so nothing snaps.
629
+ // The card fades up under it from the peak.
630
+ function rise(growMs, settleMs, sMin, sMax, rest) {
631
+ var total = growMs + settleMs, k = growMs / total;
632
+ var p = restPoint();
633
+ var covered = false;
634
+ var t0 = performance.now();
635
+ var step = function (now) {
636
+ var u = Math.min(1, (now - t0) / total);
637
+ var s = u < k ? sMin + (sMax - sMin) * (0.5 - 0.5 * Math.cos(Math.PI * u / k)) : sMax + (rest - sMax) * ease((u - k) / (1 - k));
638
+ var m = ease(u);
639
+ place(s, origin.x + (p.x - origin.x) * m, origin.y + (p.y - origin.y) * m);
640
+ lava.style.opacity = String(smooth(0, 0.1, u));
641
+ core.style.opacity = String(1 - smooth(0.04, 0.5, u));
642
+ dim.style.opacity = '0';
643
+ if (!covered && u >= k) {
644
+ covered = true;
645
+ mode = 'card';
646
+ stop();
647
+ ctx.clearRect(0, 0, w, h);
648
+ canvas.style.visibility = 'hidden';
649
+ if (onBorn) onBorn('cover');
650
+ }
651
+ if (u < 1) { requestAnimationFrame(step); return; }
652
+ settled();
653
+ };
654
+ requestAnimationFrame(step);
655
+ }
656
+
548
657
  // the small lava orb at the top, still, and itself the next tap
549
658
  function settled() {
550
659
  var p = restPoint();
@@ -587,7 +696,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
587
696
  for (var i = 0; i < dots.length; i++) { dots[i].sx = cx; dots[i].sy = cy; dots[i].vx = 0; dots[i].vy = 0; }
588
697
  pointer.inside = false;
589
698
  age = -1; timed = false; fired = false;
590
- mode = 'idle';
699
+ // after an act the next card is on its way, so the dots come out swarming and never stop
700
+ mode = fast ? 'swarm' : 'idle';
591
701
  bridgeAt = reduced ? 0 : performance.now();
592
702
  start();
593
703
  setTimeout(cb, reduced ? 0 : beatMs);
@@ -602,7 +712,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
602
712
  var t0 = performance.now();
603
713
  var step = function (now) {
604
714
  var k = Math.min(1, (now - t0) / glideMs);
605
- var t = ease(k);
715
+ var t = fast ? 0.5 - 0.5 * Math.cos(Math.PI * k) : ease(k);
606
716
  if (now - t0 >= leaveMs) takeOff();
607
717
  place(from.s + (sEnd - from.s) * t, from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t);
608
718
  core.style.opacity = String(smooth(0.3, 0.9, k));
@@ -675,6 +785,24 @@ body.has-card .foot { position: static; margin-top: 28px; }
675
785
  mode = 'idle';
676
786
  start();
677
787
  },
788
+ // the next card is on its way: the sphere is on the page and its dots keep moving
789
+ swarm: function () {
790
+ if (mode === 'card') {
791
+ canvas.style.visibility = 'visible';
792
+ measure();
793
+ for (var i = 0; i < dots.length; i++) { dots[i].sx = cx; dots[i].sy = cy; dots[i].vx = 0; dots[i].vy = 0; }
794
+ lava.classList.remove('tap', 'still', 'alive');
795
+ lava.style.visibility = 'hidden';
796
+ lava.style.opacity = '0';
797
+ lava.tabIndex = -1;
798
+ age = -1; timed = false; fired = false;
799
+ mode = 'swarm';
800
+ }
801
+ if (mode === 'idle') mode = 'swarm';
802
+ start();
803
+ },
804
+ // nothing is on its way: back to the idle drift under the pointer
805
+ calm: function () { if (mode === 'swarm') mode = 'idle'; start(); },
678
806
  melt: melt,
679
807
  leaveMs: reduced ? 200 : LEAVE_MS,
680
808
  // a bet is already up when the page loads: the settled orb, no motion
@@ -689,7 +817,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
689
817
  // card, and nothing from the card before survives a draw. Every tap posts
690
818
  // the card's id and the option's id, or the words typed in its box, to
691
819
  // /card; the Mac refuses anything else, and its answer is the next card.
692
- var bet = { shown: null, watching: null, tapAt: 0 };
820
+ var bet = { shown: null, watching: null, tapAt: 0, noting: false, clock: null };
693
821
  // while an act or a turn is under way the card takes no taps; the page
694
822
  // marks it on the body too, so anyone reading the page can tell
695
823
  var busyNow = false;
@@ -726,13 +854,16 @@ body.has-card .foot { position: static; margin-top: 28px; }
726
854
  if (lines.getClientRects().length > 1) title.classList.add('said');
727
855
  }
728
856
  if (card.body) b.appendChild(el('p', 'why', card.body));
857
+ var calm = isWait(card);
858
+ // the calm line: what the writer is doing now, how long it has been, and the notes typed here
859
+ if (calm) { b.appendChild(el('p', 'waitstep')); b.appendChild(el('span', 'waittime')); b.appendChild(el('div', 'waitnotes')); }
729
860
  // a reply keeps the title's spot; the turns so far sit quietly under it
730
- if (card.thread && card.thread.length) {
861
+ if (!calm && card.thread && card.thread.length) {
731
862
  var thread = b.appendChild(el('div', 'thread'));
732
863
  card.thread.forEach(function (t) { thread.appendChild(el('p', 'turn ' + (t.who === 'owner' ? 'you' : 'orb'), t.text)); });
733
864
  }
734
865
  // who it is and what they do; the rating and the model live on the team table
735
- if (card.job && !card.table) b.appendChild(el('p', 'jobline', [card.job.member, card.job.role].filter(Boolean).join(' · ')));
866
+ if (card.job && !card.table && !calm) b.appendChild(el('p', 'jobline', [card.job.member, card.job.role].filter(Boolean).join(' · ')));
736
867
  var inCells = card.options.filter(function (o) { return o.cell; });
737
868
  if (card.table) b.appendChild(drawTable(card, inCells));
738
869
  if (card.time) b.appendChild(el('span', 'time', card.time));
@@ -758,6 +889,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
758
889
  act.appendChild(el('p', 'links note'));
759
890
  b.appendChild(act);
760
891
  if (card.input) b.appendChild(inputLine(card.input));
892
+ if (calm) live(card, true);
761
893
  }
762
894
 
763
895
  // a table: one row per line, a line between every cell; an option with a
@@ -828,7 +960,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
828
960
  return btn;
829
961
  }
830
962
 
831
- // the card's box: a quiet line that turns into the field in its own spot
963
+ // the card's box: a quiet line that turns into the field in its own spot.
964
+ // On the calm line it is open from the start and stays open, without
965
+ // taking the cursor, and the words are a note for the idea being written.
832
966
  function inputLine(input) {
833
967
  var wrap = el('div', 'links sayline');
834
968
  var open = wrap.appendChild(el('button', 'quiet', input.label || 'or say something'));
@@ -837,6 +971,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
837
971
  if (bet.busy) return;
838
972
  typeHere(open, input.placeholder || 'say something', input.max || 280, function (text, field) { send(field, { text: text }, null); });
839
973
  });
974
+ if (box().getAttribute('data-kind') === 'wait') typeHere(open, input.placeholder || 'add a note for this idea', input.max || 280, function (text, field) { sendNote(field, text); }, true);
840
975
  return wrap;
841
976
  }
842
977
 
@@ -869,10 +1004,12 @@ body.has-card .foot { position: static; margin-top: 28px; }
869
1004
  // The tapped button becomes the field at its own size and spot, with the
870
1005
  // cursor already in it, and the other options dim. Enter or send hands
871
1006
  // the words to sent(text, field). Left empty, it turns back into the button.
872
- function typeHere(btn, placeholder, maxLength, sent) {
1007
+ // stay: the calm line's box, open from the start without the cursor, and
1008
+ // open after every send.
1009
+ function typeHere(btn, placeholder, maxLength, sent, stay) {
873
1010
  var field = el('div', btn.classList.contains('choice') ? 'choice field' : 'field');
874
1011
  // its exact height, to the subpixel, so nothing under it moves
875
- field.style.height = btn.getBoundingClientRect().height + 'px';
1012
+ if (stay) field.classList.add('stay'); else field.style.height = btn.getBoundingClientRect().height + 'px';
876
1013
  var input = el('input');
877
1014
  input.type = 'text';
878
1015
  input.maxLength = maxLength;
@@ -883,7 +1020,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
883
1020
  go.type = 'button';
884
1021
  var submit = function () {
885
1022
  var text = input.value.trim();
886
- if (text && !bet.busy) sent(text, field);
1023
+ if (text && !bet.busy && !bet.noting) sent(text, field);
887
1024
  };
888
1025
  // a press on send keeps the cursor in the field, so it never closes first
889
1026
  go.addEventListener('mousedown', function (e) { e.preventDefault(); });
@@ -892,13 +1029,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
892
1029
  if (e.key === 'Enter') { e.preventDefault(); submit(); }
893
1030
  });
894
1031
  input.addEventListener('blur', function () {
895
- if (input.value.trim() || field.classList.contains('chosen') || !field.isConnected) return;
1032
+ if (stay || input.value.trim() || field.classList.contains('chosen') || !field.isConnected) return;
896
1033
  field.replaceWith(btn);
897
1034
  dimOthers(null);
898
1035
  });
899
1036
  field.appendChild(input);
900
1037
  field.appendChild(go);
901
1038
  btn.replaceWith(field);
1039
+ if (stay) return field;
902
1040
  dimOthers(field);
903
1041
  input.focus();
904
1042
  return field;
@@ -936,9 +1074,10 @@ body.has-card .foot { position: static; margin-top: 28px; }
936
1074
  var heldAt = hold(spot);
937
1075
  var answer = api('/act', body).catch(offline);
938
1076
  if (option && IN_PLACE.indexOf(option.action) >= 0) {
1077
+ document.body.setAttribute('data-in-place', '');
939
1078
  Promise.all([answer, afterHold(heldAt)]).then(function (both) {
940
1079
  var r = both[0];
941
- if (!r.ok || !r.card) { bet.busy = false; unsent(from, r, body.text, option); return; }
1080
+ if (!r.ok || !r.card) { bet.busy = false; document.body.removeAttribute('data-in-place'); unsent(from, r, body.text, option); return; }
942
1081
  turnTo(r.card);
943
1082
  });
944
1083
  return;
@@ -956,12 +1095,12 @@ body.has-card .foot { position: static; margin-top: 28px; }
956
1095
  landed = null;
957
1096
  var wait = r.ack && lineAt ? Math.max(0, AFTER_MS - (performance.now() - lineAt)) : 0;
958
1097
  setTimeout(function () {
959
- // nothing more to show: the sphere stays, with its line
960
- if (r.ok && (r.home || !r.card)) { bet.busy = false; bet.shown = null; line.set(r.home ? 'not now. tap when you want another' : 'tap to think', false); return; }
1098
+ // nothing more to show: the sphere stays, with its line, and its dots settle into the idle drift
1099
+ if (r.ok && (r.home || !r.card)) { bet.busy = false; bet.shown = null; orb.calm(); line.set(r.home ? 'not now. tap when you want another' : 'tap to think', false); return; }
961
1100
  // the card the Mac has on screen now is the one that comes back; a
962
1101
  // refusal comes back on it with one line why
963
1102
  var then = r.card || from;
964
- if (!then) { bet.busy = false; line.set('that did not go through. tap to try again', true); return; }
1103
+ if (!then) { bet.busy = false; orb.calm(); line.set('that did not go through. tap to try again', true); return; }
965
1104
  if (!r.ok) { spiralTo(then, function () { unsent(then, r, typed, then.id === (from && from.id) ? option : null); }); return; }
966
1105
  spiralTo(then, null);
967
1106
  }, wait);
@@ -975,27 +1114,49 @@ body.has-card .foot { position: static; margin-top: 28px; }
975
1114
  }
976
1115
 
977
1116
  // the card fades in place while the small orb glides down and becomes the
978
- // sphere; the card leaves the page once it has faded
1117
+ // sphere; the card leaves the page once it has faded all the way. From the
1118
+ // calm line the sphere is already there: its words fade and it glides home.
979
1119
  function leave(then, fast) {
1120
+ if (waiting()) { leaveWait(then, fast); return; }
980
1121
  var b = box();
981
1122
  var to = sphereSpot();
982
1123
  b.classList.remove('quick', 'acted', 'up');
983
1124
  b.classList.add('leave');
1125
+ b.classList.toggle('fast', Boolean(fast));
984
1126
  var swap = function () {
985
1127
  b.style.minHeight = '';
986
1128
  b.hidden = true;
987
- b.classList.remove('leave', 'holding');
1129
+ b.classList.remove('leave', 'fast', 'holding');
988
1130
  document.body.classList.remove('has-card', 'wide');
989
1131
  window.scrollTo(0, 0);
990
1132
  };
991
- if (reduced) { swap(); orb.melt(then, to, function () {}, fast); return; }
1133
+ // reduced motion: no travel; the card and the small orb fade, then the sphere is there
1134
+ if (reduced) {
1135
+ document.getElementById('lava').style.opacity = '0';
1136
+ setTimeout(function () { swap(); stageFade(); orb.melt(then, to, function () {}, fast); }, 200);
1137
+ return;
1138
+ }
992
1139
  orb.melt(then, to, swap, fast);
993
1140
  }
994
1141
 
995
- // from the sphere: the dots spiral in and the card blooms where they land
1142
+ // under reduced motion the sphere never travels: it fades in where it now sits
1143
+ function stageFade() {
1144
+ if (!reduced) return;
1145
+ var stage = document.getElementById('stage');
1146
+ stage.style.transition = 'none';
1147
+ stage.style.opacity = '0';
1148
+ void stage.offsetWidth;
1149
+ stage.style.transition = 'opacity 0.2s linear';
1150
+ stage.style.opacity = '';
1151
+ }
1152
+
1153
+ // from the sphere: the dots spiral in and the card grows out of the point.
1154
+ // The calm line is not a card that blooms: the sphere stays and its dots keep moving.
996
1155
  function spiralTo(card, after) {
997
1156
  line.set('', false);
998
- if (reduced) { orb.settled(); showCard(card, false); bet.busy = false; if (after) after(); return; }
1157
+ if (isWait(card)) { enterWait(card); if (after) after(); return; }
1158
+ // reduced motion: no spiral; the card fades in where it goes
1159
+ if (reduced) { orb.settled(); showCard(card, true); bet.busy = false; if (after) after(); return; }
999
1160
  orb.play(function (phase) {
1000
1161
  // the card blooms where the dots land; what comes with it (a line why, the words back in their box) comes with it
1001
1162
  if (phase === 'cover') { showCard(card, true, true, true); if (after) after(); }
@@ -1012,6 +1173,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
1012
1173
  // the Mac's own refusals are plain words; anything else reads as one plain line
1013
1174
  if (note) note.textContent = (r && r.http === 409 && r.message) || (r && r.http === 0 && r.message) || 'that did not go through. try again.';
1014
1175
  if (!typed) return;
1176
+ // the calm line's box is open already: the words go back into it
1177
+ var openBox = !option && box().querySelector('.sayline .field.stay input');
1178
+ if (openBox) { openBox.value = typed; return; }
1015
1179
  // the words go back where they were typed, or to this card's own box when the card is a new one
1016
1180
  var spot = option ? box().querySelector('[data-option="' + option.id + '"]') : box().querySelector('.sayline .quiet');
1017
1181
  if (!spot) return;
@@ -1030,15 +1194,17 @@ body.has-card .foot { position: static; margin-top: 28px; }
1030
1194
 
1031
1195
  // fast: the quick fade from the big orb; act: the short one after an act
1032
1196
  function showCard(card, animate, fast, act) {
1197
+ if (isWait(card)) { enterWait(card, !animate); return; }
1033
1198
  var b = box();
1034
1199
  bet.shown = card;
1035
1200
  // where it starts from is set first: drawing measures the title, and a
1036
1201
  // measure is a frame the card would otherwise start from somewhere else
1037
- b.classList.remove('leave', 'holding');
1202
+ b.classList.remove('leave', 'fast', 'holding', 'wait-in');
1038
1203
  b.classList.toggle('quick', Boolean(fast) && !act);
1039
1204
  b.classList.toggle('acted', Boolean(act));
1040
1205
  b.classList.toggle('up', !animate);
1041
1206
  // on the page before it is drawn, so a long title can tell it runs past one line
1207
+ document.body.classList.remove('waiting');
1042
1208
  document.body.classList.add('has-card');
1043
1209
  b.hidden = false;
1044
1210
  draw(card);
@@ -1050,6 +1216,221 @@ body.has-card .foot { position: static; margin-top: 28px; }
1050
1216
  requestAnimationFrame(function () { b.classList.add('up'); });
1051
1217
  }
1052
1218
 
1219
+ // ---- the calm line: the dots keep moving while the next idea is written -----------------
1220
+ // The sphere stays on the page, a little smaller and higher, and its dots
1221
+ // swarm. The card sits under it with what the writer is doing now, and its
1222
+ // box is open: words typed there are a note for the idea being written,
1223
+ // and the card stays. When the idea lands it grows out of the dots. A tap
1224
+ // on the dots goes back to the big orb, as the small orb does on a card.
1225
+ function isWait(card) { return Boolean(card && card.kind === 'wait'); }
1226
+ function waiting() { return document.body.classList.contains('waiting'); }
1227
+ // the calm line is one card while its words change, under any stamp
1228
+ function sameCard(a, b) { return Boolean(a && b && (a.id === b.id || (isWait(a) && isWait(b) && String(a.id).split('#')[0] === String(b.id).split('#')[0]))); }
1229
+
1230
+ // the smaller sphere's place: at most 300px, and high enough that the card
1231
+ // under it has WAIT_ROOM. It is read from the sphere's own place in the
1232
+ // layout, which the calm line keeps and a transform never moves.
1233
+ var WAIT_ROOM = 250;
1234
+ function placeWait(animate) {
1235
+ var stage = document.getElementById('stage');
1236
+ var body = document.body;
1237
+ var hidden = !stage.offsetHeight;
1238
+ if (hidden) body.classList.add('has-card', 'waiting');
1239
+ var screenTop = document.getElementById('screen').getBoundingClientRect().top;
1240
+ var h = stage.offsetHeight || 1, top0 = screenTop + stage.offsetTop;
1241
+ if (hidden) body.classList.remove('waiting');
1242
+ var k = Math.min(1, 300 / h, 0.62 * Math.min(window.innerWidth, window.innerHeight) / h);
1243
+ var top = Math.max(24, (window.innerHeight - (k * h + WAIT_ROOM)) / 2);
1244
+ var s = body.style;
1245
+ s.setProperty('--wait-k', k.toFixed(4));
1246
+ s.setProperty('--wait-dy', Math.round(top - top0) + 'px');
1247
+ s.setProperty('--wait-top', Math.round(top + k * h + 6 - screenTop) + 'px');
1248
+ if (animate) return;
1249
+ stage.style.transition = 'none';
1250
+ requestAnimationFrame(function () { requestAnimationFrame(function () { stage.style.transition = ''; }); });
1251
+ }
1252
+
1253
+ // a turned phone: the calm line's sphere and card find their place again
1254
+ window.addEventListener('resize', function () { if (waiting()) placeWait(false); });
1255
+
1256
+ // atOnce: the page opened on it, so it is simply there
1257
+ function enterWait(card, atOnce) {
1258
+ var b = box();
1259
+ var was = waiting() && !b.hidden;
1260
+ bet.shown = card;
1261
+ if (atOnce) line.clear(); else line.set('', false);
1262
+ orb.swarm();
1263
+ if (!was) { placeWait(!atOnce && !reduced); stageFade(); }
1264
+ b.classList.remove('leave', 'fast', 'holding', 'quick', 'acted', 'up', 'wait-in');
1265
+ document.body.classList.add('has-card', 'waiting');
1266
+ document.getElementById('orb').setAttribute('aria-label', 'the atris orb, writing your next idea. tap to go back to the big orb');
1267
+ b.hidden = false;
1268
+ draw(card);
1269
+ watchWait();
1270
+ bet.busy = false;
1271
+ if (was || atOnce) { b.classList.add('up'); return; }
1272
+ b.classList.add('wait-in');
1273
+ void b.offsetWidth;
1274
+ requestAnimationFrame(function () { b.classList.add('up'); });
1275
+ }
1276
+
1277
+ // The calm line's live words: what the writer is doing now, how long it
1278
+ // has been, and the newest note typed here with the answer to it. draw()
1279
+ // puts them on the card; while it shows they are kept fresh in place. Its
1280
+ // options and its box never change under one id, so the field, its words,
1281
+ // and the cursor in it are never touched.
1282
+ function live(card, atOnce) {
1283
+ var b = box();
1284
+ var step = b.querySelector('.waitstep');
1285
+ if (!isWait(card) || !step) return;
1286
+ bet.shown = card;
1287
+ b.setAttribute('data-card', card.id);
1288
+ var h = b.querySelector('h1');
1289
+ if (h.textContent !== card.title) h.textContent = card.title;
1290
+ var lines = Array.isArray(card.progress) ? card.progress : [];
1291
+ var newest = lines.length ? lines[lines.length - 1] : null;
1292
+ stepTo(step, newest && typeof newest.line === 'string' ? newest.line : '', atOnce);
1293
+ notesTo(b.querySelector('.waitnotes'), card.thread || [], atOnce);
1294
+ // the Mac's own count of how long it has been, carried on from when it came
1295
+ if (Number.isFinite(card.waited_ms)) waitClock = { key: String(card.id).split('#')[0], ms: card.waited_ms, at: Date.now() };
1296
+ tickWait();
1297
+ }
1298
+
1299
+ // one line at a time, crossfaded in its own spot, so nothing under it moves
1300
+ function stepTo(node, text, atOnce) {
1301
+ if (node.getAttribute('data-text') === text) return;
1302
+ node.setAttribute('data-text', text);
1303
+ clearTimeout(node.fading);
1304
+ if (atOnce || reduced || !node.textContent) { node.textContent = text; node.classList.remove('out'); return; }
1305
+ node.classList.add('out');
1306
+ node.fading = setTimeout(function () { node.textContent = node.getAttribute('data-text'); node.classList.remove('out'); }, 260);
1307
+ }
1308
+
1309
+ // the newest note and the answer to it; a new one fades in, and the room it
1310
+ // takes opens smoothly
1311
+ function notesTo(node, turns, atOnce) {
1312
+ var want = turns.slice(-2);
1313
+ var had = Array.prototype.map.call(node.children, function (n) { return n.textContent; });
1314
+ if (had.join('\\n') === want.map(function (t) { return t.text; }).join('\\n')) return;
1315
+ var from = node.offsetHeight;
1316
+ node.style.height = '';
1317
+ node.textContent = '';
1318
+ want.forEach(function (t, i) {
1319
+ var p = node.appendChild(el('p', 'turn ' + (t.who === 'owner' ? 'you' : 'orb'), t.text));
1320
+ if (!atOnce && !reduced && had[i] !== t.text) p.classList.add('fresh');
1321
+ });
1322
+ if (atOnce || reduced) return;
1323
+ var to = node.offsetHeight;
1324
+ if (from !== to) {
1325
+ node.style.height = from + 'px';
1326
+ void node.offsetHeight;
1327
+ node.style.height = to + 'px';
1328
+ clearTimeout(node.opening);
1329
+ node.opening = setTimeout(function () { node.style.height = ''; }, 340);
1330
+ }
1331
+ requestAnimationFrame(function () { requestAnimationFrame(function () { Array.prototype.forEach.call(node.querySelectorAll('.fresh'), function (p) { p.classList.remove('fresh'); }); }); });
1332
+ }
1333
+
1334
+ // how long the idea has been written, in plain words: the Mac's own count
1335
+ // when the card has it, else from when it began on the Mac's clock when
1336
+ // that is sane, else from when the page first saw the calm line
1337
+ var seen = { key: null, at: 0 };
1338
+ var waitClock = { key: null, ms: 0, at: 0 };
1339
+ function sinceWords(ms) {
1340
+ var s = Math.round(ms / 1000);
1341
+ if (!(s >= 5) || s > 3600) return '';
1342
+ if (s < 60) return Math.max(5, Math.round(s / 5) * 5) + ' seconds so far';
1343
+ var m = Math.round(s / 60);
1344
+ return m === 1 ? 'about a minute so far' : 'about ' + m + ' minutes so far';
1345
+ }
1346
+ function tickWait() {
1347
+ var node = box().querySelector('.waittime');
1348
+ var card = bet.shown;
1349
+ if (!node || !isWait(card) || !waiting()) { clearInterval(bet.clock); bet.clock = null; return; }
1350
+ var key = String(card.id).split('#')[0];
1351
+ if (seen.key !== key) seen = { key: key, at: Date.now() };
1352
+ var began = typeof card.started_at === 'number' ? card.started_at : Date.parse(card.started_at || '');
1353
+ var ms = waitClock.key === key ? waitClock.ms + (Date.now() - waitClock.at) : Date.now() - began;
1354
+ if (!(ms >= 0 && ms < 3600000)) ms = Date.now() - seen.at;
1355
+ var words = sinceWords(ms);
1356
+ if (node.textContent !== words) node.textContent = words;
1357
+ if (!bet.clock) bet.clock = setInterval(tickWait, 1000);
1358
+ }
1359
+
1360
+ // A note on the calm line: the words hold in the box for the moment it
1361
+ // takes, then the box clears and stays open, and the note and the answer
1362
+ // to it show above. Nothing moves on: the idea that comes in is the one
1363
+ // written with it. Words that did not reach the Mac stay in the box.
1364
+ function sendNote(field, text) {
1365
+ var from = bet.shown;
1366
+ var input = field.querySelector('input');
1367
+ var body = { card_id: from.id, text: text, send: sendId([from.id, 'the box', text].join(' | ')) };
1368
+ bet.noting = true;
1369
+ field.classList.add('chosen');
1370
+ input.readOnly = true;
1371
+ var note = box().querySelector('.act .note');
1372
+ if (note) note.textContent = '';
1373
+ api('/act', body).catch(offline).then(function (r) {
1374
+ bet.noting = false;
1375
+ field.classList.remove('chosen');
1376
+ input.readOnly = false;
1377
+ // the owner went back to the big orb meanwhile: the Mac has the note, and nothing here moves
1378
+ if (!waiting()) return;
1379
+ if (r.ok && r.card && sameCard(r.card, from)) { input.value = ''; input.blur(); live(r.card); return; }
1380
+ // the Mac moved on (the idea landed meanwhile): it comes in, and the words go back in its box
1381
+ if (r.card && !sameCard(r.card, from)) { landFromWait(r.card, r.ok ? null : function () { unsent(r.card, r, text, null); }); return; }
1382
+ if (note) note.textContent = (r && r.message) || 'that did not go through. try again.';
1383
+ });
1384
+ }
1385
+
1386
+ // off the calm line, home or to an act's next card: its words fade, then
1387
+ // the sphere glides back to its own place and size
1388
+ function leaveWait(then, fast) {
1389
+ var b = box();
1390
+ stopWatch();
1391
+ if (!fast) orb.calm();
1392
+ b.classList.remove('wait-in');
1393
+ b.classList.add('leave', 'fast');
1394
+ setTimeout(function () {
1395
+ b.hidden = true;
1396
+ b.classList.remove('leave', 'fast', 'holding', 'up');
1397
+ document.body.classList.remove('has-card', 'wide', 'waiting');
1398
+ stageFade();
1399
+ document.getElementById('orb').setAttribute('aria-label', 'the atris orb. tap to think of one new idea for the business');
1400
+ then();
1401
+ }, 220);
1402
+ }
1403
+
1404
+ // The idea lands: the calm line's words fade while the dots spiral in
1405
+ // from wherever they are, and the card grows out of the point.
1406
+ function landFromWait(card, after) {
1407
+ var b = box();
1408
+ bet.busy = true;
1409
+ stopWatch();
1410
+ b.classList.remove('wait-in');
1411
+ b.classList.add('leave', 'fast');
1412
+ var off = function () {
1413
+ b.classList.remove('leave', 'fast');
1414
+ document.body.classList.remove('waiting');
1415
+ document.getElementById('orb').setAttribute('aria-label', 'the atris orb. tap to think of one new idea for the business');
1416
+ };
1417
+ if (reduced) {
1418
+ setTimeout(function () { off(); orb.settled(); showCard(card, true); bet.busy = false; if (after) after(); }, 200);
1419
+ return;
1420
+ }
1421
+ orb.play(function (phase) {
1422
+ if (phase === 'cover') { off(); showCard(card, true, true, true); if (after) after(); }
1423
+ if (phase === 'settled') bet.busy = false;
1424
+ }, true, true);
1425
+ }
1426
+
1427
+ // a card the Mac has now that is not the one on screen
1428
+ function arrive(card) {
1429
+ if (waiting()) { landFromWait(card); return; }
1430
+ if (isWait(card)) { leave(function () { enterWait(card); }, true); return; }
1431
+ turnTo(card);
1432
+ }
1433
+
1053
1434
  // where the sphere sits once the card is gone, read before anything moves,
1054
1435
  // in page coordinates (the screen's once the page is back at the top)
1055
1436
  function sphereSpot() {
@@ -1075,7 +1456,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
1075
1456
  var b = box();
1076
1457
  b.classList.remove('holding');
1077
1458
  bet.shown = card;
1078
- turnInPlace(b, function () { draw(card); }, function () { bet.busy = false; measured(); if (card.watch) watchWait(); else stopWatch(); });
1459
+ document.body.setAttribute('data-in-place', '');
1460
+ turnInPlace(b, function () { draw(card); }, function () { bet.busy = false; document.body.removeAttribute('data-in-place'); measured(); if (card.watch) watchWait(); else stopWatch(); });
1079
1461
  }
1080
1462
 
1081
1463
  function turnInPlace(b, paint, after) {
@@ -1113,10 +1495,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
1113
1495
  }, 80 + TURN_IN_MS + 20);
1114
1496
  }
1115
1497
 
1116
- // While the calm line shows, look every WAIT_LOOK_MS for the idea; once the
1117
- // Mac has one ready, it is asked for the way a tap does and turns in with
1118
- // one crossfade. It waits while the owner types there (the box has the
1119
- // cursor or any words), and on the big orb it waits for their tap.
1498
+ // While the calm line shows, look every WAIT_LOOK_MS: its live words are
1499
+ // kept fresh, even while the owner types, and once the Mac has the idea
1500
+ // ready it is asked for the way a tap does and grows out of the dots. It
1501
+ // waits while the owner types there (the box has the cursor or any words)
1502
+ // or a note is on its way, and on the big orb it waits for their tap.
1120
1503
  var WAIT_LOOK_MS = 1500;
1121
1504
  function typingOnCard() {
1122
1505
  var input = box().querySelector('.field input');
@@ -1125,30 +1508,32 @@ body.has-card .foot { position: static; margin-top: 28px; }
1125
1508
  function stopWatch() { if (bet.watching) { clearInterval(bet.watching); bet.watching = null; } }
1126
1509
  function watchWait() {
1127
1510
  if (bet.watching) return;
1128
- var quiet = function () { return !bet.busy && document.body.classList.contains('has-card') && !typingOnCard(); };
1511
+ var quiet = function () { return !bet.busy && !bet.noting && document.body.classList.contains('has-card') && !typingOnCard(); };
1129
1512
  bet.watching = setInterval(function () {
1130
1513
  if (!bet.shown || !bet.shown.watch) { stopWatch(); return; }
1131
- if (!quiet()) return;
1514
+ if (bet.busy || bet.noting) return;
1132
1515
  api('/card').then(function (s) {
1133
- if (s.http !== 200 || !quiet() || !bet.shown || !bet.shown.watch || !s.card) return;
1134
- if (s.card.id !== bet.shown.id && isLive(s)) { turnTo(s.card); return; }
1135
- if (s.queue > 0) {
1136
- bet.busy = true;
1137
- api('/act', { orb: true, card_id: bet.shown.id }).catch(offline).then(function (r) {
1138
- if (r.ok && r.card) turnTo(r.card); else bet.busy = false;
1139
- });
1140
- }
1516
+ if (s.http !== 200 || !s.card || !bet.shown || !bet.shown.watch || bet.busy || bet.noting) return;
1517
+ if (sameCard(s.card, bet.shown)) live(s.card);
1518
+ else if (quiet()) { bet.busy = true; arrive(s.card); return; }
1519
+ if (!quiet() || !(s.queue > 0)) return;
1520
+ bet.busy = true;
1521
+ api('/act', { orb: true, card_id: bet.shown.id }).catch(offline).then(function (r) {
1522
+ if (r.ok && r.card && !sameCard(r.card, bet.shown)) { arrive(r.card); return; }
1523
+ bet.busy = false;
1524
+ if (r.ok && r.card) live(r.card);
1525
+ });
1141
1526
  }).catch(function () { /* the next look tries again */ });
1142
1527
  }, WAIT_LOOK_MS);
1143
1528
  }
1144
1529
 
1145
- function isLive(s) { return Boolean(s && s.card); }
1146
-
1147
1530
  // ---- the orb ----------------------------------------------------------------------------
1148
1531
  // The big orb: the next card, with the spiral, as on page open. An idea
1149
1532
  // that landed behind the calm line is the next one, so it is never passed.
1150
1533
  function think() {
1151
1534
  if (bet.busy) return;
1535
+ // on the calm line the dots are the orb on the card: a tap goes back to the big one
1536
+ if (waiting()) { goHome(); return; }
1152
1537
  buzz();
1153
1538
  bet.busy = true;
1154
1539
  bet.tapAt = performance.now();
@@ -1161,6 +1546,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
1161
1546
  api('/act', { orb: true, card_id: bet.shown ? bet.shown.id : null }).then(function (r) {
1162
1547
  if (r.http !== 200 || !r.card) { if (r.http === 200) { bet.busy = false; orb.relax(); line.set('tap to think', false); return; } failed(); return; }
1163
1548
  line.set('', false);
1549
+ // nothing ready yet: the calm line, with the sphere where it is and its dots moving
1550
+ if (isWait(r.card)) { enterWait(r.card); return; }
1164
1551
  orb.play(function (phase) { if (phase === 'settled') { bet.busy = false; showCard(r.card, true, true); } }, true);
1165
1552
  }, failed);
1166
1553
  }
@@ -1174,6 +1561,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
1174
1561
  buzz();
1175
1562
  bet.busy = true;
1176
1563
  stopWatch();
1564
+ // from the calm line the sphere is already there; its line comes once the card's words are gone
1565
+ if (waiting()) { leave(function () { line.set('tap to think', false); bet.busy = false; }); return; }
1177
1566
  // the line under the big orb is ready before the sphere is back
1178
1567
  line.set('tap to think', false);
1179
1568
  leave(function () { bet.busy = false; });
@@ -1196,6 +1585,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
1196
1585
  function firstLook() {
1197
1586
  api('/card').then(function (s) {
1198
1587
  if (s.http !== 200) return;
1588
+ if (s.card && isWait(s.card)) { enterWait(s.card, true); return; }
1199
1589
  if (s.card) { orb.settled(); showCard(s.card, false); return; }
1200
1590
  line.set(s.status === 'skipped' ? 'not now. tap when you want another' : 'tap to think', false);
1201
1591
  });
@@ -1274,14 +1664,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
1274
1664
 
1275
1665
  // Every few seconds: the footer counts, and the card on screen. When the
1276
1666
  // Mac's card has changed (a new id: its options are not the ones shown),
1277
- // it turns in place, unless the owner is typing or a motion is under way.
1667
+ // it turns in place, or grows out of the dots on the calm line, unless the
1668
+ // owner is typing or a motion is under way.
1278
1669
  function refresh() {
1279
1670
  return api('/card').then(function (s) {
1280
1671
  if (s.http !== 200) { line.set('this link stopped working. start gm phone again on the mac.', true); return; }
1281
1672
  drawWorking(s.team_working || 0);
1282
- if (s.card && bet.shown && s.card.id !== bet.shown.id && !bet.busy && !typingOnCard() && document.body.classList.contains('has-card')) {
1673
+ if (s.card && bet.shown && !sameCard(s.card, bet.shown) && !bet.busy && !bet.noting && !typingOnCard() && document.body.classList.contains('has-card')) {
1283
1674
  bet.busy = true;
1284
- turnTo(s.card);
1675
+ arrive(s.card);
1285
1676
  }
1286
1677
  }).catch(function () { /* the next refresh tries again */ });
1287
1678
  }