@bongos/core 1.21.109 → 1.21.110

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.
@@ -72,6 +72,34 @@ Heal whatever ship-check names first (`git add` new files before it). Run ship.j
72
72
 
73
73
  A failed grade is an ordinary one: read it (`GET /api/bongos/tasks/<task-id>?include=grade`), fix, commit, `node scripts/gds/ship.js <task-id> --regrade`. If the branch did not publish, a plain `node scripts/gds/ship.js <task-id>` publishes it again without re-grading.
74
74
 
75
+ ## Unattended mode: `/tweak --page <page-id> --unattended`
76
+
77
+ The Autobongos tweak lane (ADR 0371 D1, D8) runs this skill with nobody at the terminal. It names ONE page and claims that page's submitted round with the `auto_pickup` mark before you start. In this mode:
78
+
79
+ - **Never ask.** Every question the steps above would put to a person is a halt instead.
80
+ - **Work only the named page.** Skip step 1's queue read; the round is the one you hold. Pass `--page <page-id>` to every script: `copy-apply.js --batch --page <page-id> --unattended` refuses a round for another page (exit 5) and, on any non-zero exit, releases the round with a plain note itself.
81
+ - **Change only the page's own files.** No shell cache-bust, no hand fix of a refused line, no edit outside the page's files in `docs/page-inventory.json` (besides the copy registry, the page readings and the generated maps ship.js refreshes), and no session-log file (it would move `CLAUDE.md`, a protected path). A rewrite that needs anything else is already its own engineer task.
82
+ - **Ship one round, once, graded.** Never `--skip-grade`, `--force` or `--allow-empty`. Stop at WAITING FOR THE ARTIST.
83
+
84
+ **On any halt, release and exit.** Run the matching line and stop; do not retry, and do not try another round:
85
+
86
+ ```bash
87
+ node scripts/gds/tweak-halt.js <task-id> --code <code>
88
+ ```
89
+
90
+ | What happened | `--code` |
91
+ |---|---|
92
+ | the claim was refused | `claim_refused` (nothing to release: just exit) |
93
+ | `copy-apply` exited non-zero | already released by `--unattended` |
94
+ | `ship-check` named something stale it cannot heal by regenerating | `ship_check_stale` |
95
+ | the grade came back GRADER UNAVAILABLE | `grader_unavailable` |
96
+ | the grade failed | `grade_failed` |
97
+ | the diff would touch a file outside the page | `outside_page` |
98
+
99
+ The note is fixed wording per code, so nothing you write reaches the round.
100
+
101
+ **The server holds these rules, not you.** A page tweak reaches `confirmed` only through its artist's approval or the owner's override (`POST /tasks/:id/confirm` refuses it), and the publish of an `auto_pickup` round whose pull request touches a file outside the page, or a protected path, is refused and its pull request closed. Breaking a rule above gets the round refused, not landed.
102
+
75
103
  ## What happens next (not yours)
76
104
 
77
105
  The artist approves in the studio's Approval queue (TW12), which lands it like any confirmed task, or sends it back with a note, which returns the round to the queue for the next `/tweak`.
@@ -9,7 +9,9 @@
9
9
  // It is a MODE the existing hall wears, never a second hall (the founding-mode
10
10
  // precedent, docs/design/project-startup-direction.md). The nav follows the mode
11
11
  // (task 1004493, MODE_TABS below), and so does the look (task 1004495, LOOKS
12
- // below); the mode tools are their own tasks (1004496 onward). Legacy mode, a
12
+ // below); the mode tools are their own tasks (1004496 onward), and Artistry's
13
+ // Review button opens review mode (task 1004686): the hall wears another
14
+ // mode's experience while an artist reviews it. Legacy mode, a
13
15
  // fifth choice, was removed (task 1004567): Building already shows every tab,
14
16
  // and a stored 'legacy' now reads as no mode.
15
17
  //
@@ -32,6 +34,7 @@
32
34
  governing: '<path d="M2 6 8 2.5 14 6M3.5 6.5v6M6.5 6.5v6M9.5 6.5v6M12.5 6.5v6M2 13.5h12"/>',
33
35
  brush: '<path d="M13.6 2.4 7.4 8.6"/><path d="M7.4 8.6c-1.3-.7-3-.1-3.4 1.5-.3 1.3-.8 2.2-1.9 2.9 2.4.7 4.6.1 5.6-1.4.6-.9.5-2.1-.3-3Z"/>',
34
36
  note: '<path d="M9.5 2.5h-6v11h9v-6"/><path d="M5.5 8h5M5.5 10.5h3.5"/><path d="M12.5 1.5v4M10.5 3.5h4"/>',
37
+ write: '<path d="M2.5 13.5h11"/><path d="M10.4 2.6l2.9 2.9-6.4 6.4H4V9z"/>',
35
38
  modes: '<rect x="2.5" y="2.5" width="4.5" height="4.5" rx="1"/><rect x="9" y="2.5" width="4.5" height="4.5" rx="1"/><rect x="2.5" y="9" width="4.5" height="4.5" rx="1"/><rect x="9" y="9" width="4.5" height="4.5" rx="1"/>',
36
39
  };
37
40
  // The four crafts. `short` is the switch's label; `line` is the
@@ -266,7 +269,8 @@
266
269
  artistry: [
267
270
  // Named "Artist review" since task 1004499: the public site's button
268
271
  // (public-landing assets/artist-review.js) opens this same panel.
269
- { id: 'palette', icon: 'artistry', short: 'Review', label: 'Artist review: write feedback on this page', script: 'page-notes.js', api: 'OTBPageNotes' },
272
+ // `menu`: hovering it opens the review-mode menu (task 1004686, below).
273
+ { id: 'palette', icon: 'artistry', short: 'Review', label: 'Artist review: write feedback on this page', script: 'page-notes.js', api: 'OTBPageNotes', menu: true },
270
274
  // Task 1004497: this page's words, edited where they stand as a proposed wording change.
271
275
  { id: 'paintbrush', icon: 'brush', short: 'Edit words', label: 'Edit this page\'s words', script: 'paintbrush.js', api: 'OTBPaintbrush' },
272
276
  ],
@@ -297,29 +301,270 @@
297
301
  for (var m in TOOLS) for (var i = 0; i < TOOLS[m].length; i++) if (TOOLS[m][i].id === id) return TOOLS[m][i];
298
302
  return null;
299
303
  }
304
+ // The tools a mode shows. Reviewing another experience (task 1004686), the
305
+ // artist keeps Artistry's tools and gains that experience's own after them.
306
+ function toolsFor(mode) {
307
+ var own = Object.prototype.hasOwnProperty.call(TOOLS, mode) ? TOOLS[mode] : [];
308
+ if (!review || mode === 'artistry') return own;
309
+ return TOOLS.artistry.concat(own);
310
+ }
311
+ function openTool(t, b) {
312
+ loadTool(t).then(function (api) { api.open(); }, function () { b.setAttribute('aria-disabled', 'true'); });
313
+ }
314
+ function toolButton(t) {
315
+ var b = document.createElement('button');
316
+ b.type = 'button';
317
+ b.id = 'craft-tool-' + t.id;
318
+ b.className = 'craft-tool craft-tool--' + t.id;
319
+ b.setAttribute('data-craft-tool', t.id);
320
+ b.setAttribute('aria-label', t.label);
321
+ b.title = t.label;
322
+ b.innerHTML = icon(t.icon, 'craft-tool__icon') + '<span class="craft-tool__label">' + esc(t.short) + '</span>';
323
+ b.addEventListener('click', function (e) {
324
+ // A phone has no hover, so there a tap on Review opens its menu instead.
325
+ if (t.menu && narrow()) { toggleReviewMenu(e && e.detail === 0); return; }
326
+ openTool(t, b);
327
+ });
328
+ return t.menu ? reviewGroup(t, b) : b;
329
+ }
330
+ // The node a tool occupies in the bar: its button, or Review's whole group.
331
+ function toolNode(t) {
332
+ var b = document.getElementById('craft-tool-' + t.id);
333
+ return b && t.menu && b.parentNode && b.parentNode.id === REVIEW_GROUP ? b.parentNode : b;
334
+ }
300
335
  function applyTools(mode) {
301
336
  var sw = document.getElementById('craft-switch');
302
- var own = Object.prototype.hasOwnProperty.call(TOOLS, mode) ? TOOLS[mode] : [];
337
+ var own = toolsFor(mode);
303
338
  var old = document.querySelectorAll ? document.querySelectorAll('[data-craft-tool]') : [];
304
339
  for (var i = old.length - 1; i >= 0; i--) {
305
- if (!own.some(function (t) { return t.id === old[i].getAttribute('data-craft-tool'); })) remove(old[i]);
340
+ if (own.some(function (t) { return t.id === old[i].getAttribute('data-craft-tool'); })) continue;
341
+ var t0 = toolFor(old[i].getAttribute('data-craft-tool'));
342
+ remove(t0 ? toolNode(t0) : old[i]);
306
343
  }
307
344
  if (!sw || !sw.parentNode) return;
345
+ // Right to left, each missing tool goes in front of the one after it, so
346
+ // the bar reads in TOOLS order whichever tools were already there.
347
+ var before = sw;
348
+ for (var j = own.length - 1; j >= 0; j--) {
349
+ var node = toolNode(own[j]);
350
+ if (!node) node = sw.parentNode.insertBefore(toolButton(own[j]), before);
351
+ before = node;
352
+ }
353
+ // A guest is the reviewed experience's own tool beside the artist's: under
354
+ // 360px the bar has no room for three, so craft-review.css folds it away.
308
355
  own.forEach(function (t) {
309
- if (document.getElementById('craft-tool-' + t.id)) return;
310
- var b = document.createElement('button');
311
- b.type = 'button';
312
- b.id = 'craft-tool-' + t.id;
313
- b.className = 'craft-tool craft-tool--' + t.id;
314
- b.setAttribute('data-craft-tool', t.id);
315
- b.setAttribute('aria-label', t.label);
316
- b.title = t.label;
317
- b.innerHTML = icon(t.icon, 'craft-tool__icon') + '<span class="craft-tool__label">' + esc(t.short) + '</span>';
318
- b.addEventListener('click', function () {
319
- loadTool(t).then(function (api) { api.open(); }, function () { b.setAttribute('aria-disabled', 'true'); });
320
- });
321
- sw.parentNode.insertBefore(b, sw);
356
+ var el = document.getElementById('craft-tool-' + t.id);
357
+ if (el && el.classList) el.classList.toggle('craft-tool--guest', !!review && TOOLS.artistry.indexOf(t) === -1);
322
358
  });
359
+ var on = document.getElementById('craft-tool-palette');
360
+ if (on) {
361
+ on.classList.toggle('craft-tool--reviewing', !!review);
362
+ var word = on.querySelector && on.querySelector('.craft-tool__label');
363
+ if (word) word.textContent = review ? 'Reviewing' : 'Review';
364
+ }
365
+ }
366
+
367
+ // ---------- artist review mode (task 1004686) ----------
368
+ // Hovering Artistry's Review button opens a menu of the four experiences
369
+ // (owner, 2026-10-09, in the owner's order and words). Picking one makes the hall
370
+ // wear that mode on every page — its tabs, its look, its tools — the way a
371
+ // builder in it sees it, while the artist keeps Artistry's tools and small
372
+ // sky-style doodles are scribbled into the top bar's empty space (craft-review.js draws them,
373
+ // loaded only in review mode; craft-review.css styles it and the menu).
374
+ // The artist's own mode stays Artistry: the experience is this session's
375
+ // alone (sessionStorage), and choosing any mode from the switch or the
376
+ // picker ends it. Cosmetic like the mode: a tab the gate hides stays hidden.
377
+ var REVIEW_KEY = 'hall-craft-review';
378
+ var REVIEW_GROUP = 'craft-review';
379
+ var EXPERIENCES = [
380
+ { id: 'ideating', label: 'Ideator experience' },
381
+ { id: 'governing', label: 'Governing experience' },
382
+ { id: 'building', label: 'Building experience' },
383
+ { id: 'artistry', label: 'Artistry experience' },
384
+ ];
385
+ // The experience being reviewed, or null. Read from the session once, then
386
+ // held here, so a browser whose storage throws still reviews this page.
387
+ var review = null;
388
+ function storedReview() {
389
+ return read() === 'artistry' ? valid(get('sessionStorage', REVIEW_KEY)) : null;
390
+ }
391
+ // The mode the hall wears: the experience under review, else the chosen mode.
392
+ function wearing() { return review || read(); }
393
+ function startReview(mode) {
394
+ mode = valid(mode);
395
+ if (!mode) return;
396
+ set('sessionStorage', MODE_KEY, 'artistry');
397
+ set('sessionStorage', ASKED_KEY, '1');
398
+ set('sessionStorage', REVIEW_KEY, mode);
399
+ review = mode;
400
+ apply(mode);
401
+ }
402
+ function stopReview() {
403
+ drop('sessionStorage', REVIEW_KEY);
404
+ review = null;
405
+ apply('artistry');
406
+ }
407
+
408
+ // The menu. Hover opens it where a pointer can hover; the chevron (or, on a
409
+ // phone, Review itself) opens it by click, tap or keyboard. Built on every
410
+ // open, so the checked experience is always the live one.
411
+ var closeTimer = null;
412
+ // Hover is a mouse's alone: a touch also fires pointer events, and a laptop
413
+ // with a touch screen can report no hover at all, so ask the pointer itself.
414
+ function byMouse(e) { return !!e && e.pointerType === 'mouse'; }
415
+ function reviewMenuHtml() {
416
+ var items = '<button class="craft-review__item" type="button" role="menuitem" data-review-act="write">' +
417
+ icon('write', 'craft-review__icon') + '<span>Write feedback on this page</span></button>' +
418
+ '<p class="craft-review__head" role="presentation">Review mode</p>';
419
+ items += EXPERIENCES.map(function (x) {
420
+ var on = review === x.id;
421
+ return '<button class="craft-review__item" type="button" role="menuitemradio" aria-checked="' + (on ? 'true' : 'false') + '" data-review-pick="' + x.id + '">' +
422
+ icon(x.id, 'craft-review__icon') + '<span>' + esc(x.label) + '</span>' +
423
+ (on ? '<svg class="craft-review__check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>' : '') +
424
+ '</button>';
425
+ }).join('');
426
+ if (review) {
427
+ items += '<button class="craft-review__item craft-review__item--stop" type="button" role="menuitem" data-review-act="stop">' +
428
+ '<svg class="craft-review__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/></svg><span>Stop reviewing</span></button>';
429
+ }
430
+ return items;
431
+ }
432
+ function menuEl() { return document.getElementById(REVIEW_GROUP + '-menu'); }
433
+ function menuItems() {
434
+ var m = menuEl();
435
+ return m && m.querySelectorAll ? Array.prototype.slice.call(m.querySelectorAll('[role^="menuitem"]')) : [];
436
+ }
437
+ function openReviewMenu(focusFirst) {
438
+ var m = menuEl(), more = document.getElementById(REVIEW_GROUP + '-more');
439
+ if (!m) return;
440
+ clearTimeout(closeTimer);
441
+ if (m.hasAttribute('hidden')) {
442
+ m.innerHTML = reviewMenuHtml();
443
+ m.removeAttribute('hidden');
444
+ if (more) more.setAttribute('aria-expanded', 'true');
445
+ document.addEventListener('click', onOutside, true);
446
+ }
447
+ if (focusFirst) {
448
+ var first = menuItems()[0];
449
+ if (first && first.focus) first.focus();
450
+ }
451
+ }
452
+ function closeReviewMenu(refocus) {
453
+ var m = menuEl(), more = document.getElementById(REVIEW_GROUP + '-more');
454
+ clearTimeout(closeTimer);
455
+ document.removeEventListener('click', onOutside, true);
456
+ if (!m || m.hasAttribute('hidden')) return;
457
+ m.setAttribute('hidden', '');
458
+ if (more) more.setAttribute('aria-expanded', 'false');
459
+ if (refocus) {
460
+ var back = narrow() ? document.getElementById('craft-tool-palette') : more;
461
+ if (back && back.focus) back.focus();
462
+ }
463
+ }
464
+ function toggleReviewMenu(focusFirst) {
465
+ var m = menuEl();
466
+ if (m && !m.hasAttribute('hidden')) closeReviewMenu(false); else openReviewMenu(focusFirst);
467
+ }
468
+ function onOutside(e) {
469
+ var g = document.getElementById(REVIEW_GROUP);
470
+ if (g && g.contains && !g.contains(e.target)) closeReviewMenu(false);
471
+ }
472
+ function onMenuClick(e) {
473
+ var btn = e.target.closest && e.target.closest('button');
474
+ if (!btn) return;
475
+ var pick = btn.getAttribute('data-review-pick');
476
+ var act = btn.getAttribute('data-review-act');
477
+ if (!pick && !act) return;
478
+ closeReviewMenu(false);
479
+ if (pick) startReview(pick);
480
+ else if (act === 'stop') stopReview();
481
+ else if (act === 'write') openTool(toolFor('palette'), document.getElementById('craft-tool-palette'));
482
+ }
483
+ // The menu keys: arrows move, Home and End jump, Escape closes back to the
484
+ // button that opened it, Tab closes and lets focus move on.
485
+ function onMenuKey(e) {
486
+ var items = menuItems();
487
+ var at = items.indexOf(document.activeElement);
488
+ var to = null;
489
+ if (e.key === 'ArrowDown') to = (at + 1) % items.length;
490
+ else if (e.key === 'ArrowUp') to = (at - 1 + items.length) % items.length;
491
+ else if (e.key === 'Home') to = 0;
492
+ else if (e.key === 'End') to = items.length - 1;
493
+ else if (e.key === 'Escape') { e.preventDefault(); closeReviewMenu(true); return; }
494
+ else if (e.key === 'Tab') { closeReviewMenu(false); return; }
495
+ if (to === null || !items[to]) return;
496
+ e.preventDefault();
497
+ items[to].focus();
498
+ }
499
+ function reviewGroup(t, b) {
500
+ var g = document.createElement('span');
501
+ g.id = REVIEW_GROUP;
502
+ g.className = 'craft-review';
503
+ var more = document.createElement('button');
504
+ more.type = 'button';
505
+ more.id = REVIEW_GROUP + '-more';
506
+ more.className = 'craft-review__more';
507
+ more.setAttribute('aria-haspopup', 'menu');
508
+ more.setAttribute('aria-expanded', 'false');
509
+ more.setAttribute('aria-controls', REVIEW_GROUP + '-menu');
510
+ more.setAttribute('aria-label', 'Review mode: see the hall as another mode');
511
+ more.title = 'Review mode: see the hall as another mode';
512
+ more.innerHTML = '<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4.5 6 7.5 9 4.5"/></svg>';
513
+ more.addEventListener('click', function (e) { toggleReviewMenu(e && e.detail === 0); });
514
+ more.addEventListener('keydown', function (e) {
515
+ if (e.key === 'ArrowDown') { e.preventDefault(); openReviewMenu(true); }
516
+ });
517
+ var menu = document.createElement('div');
518
+ menu.id = REVIEW_GROUP + '-menu';
519
+ menu.className = 'craft-review__menu';
520
+ menu.setAttribute('role', 'menu');
521
+ menu.setAttribute('aria-label', 'Review mode');
522
+ menu.setAttribute('hidden', '');
523
+ menu.addEventListener('click', onMenuClick);
524
+ menu.addEventListener('keydown', onMenuKey);
525
+ b.addEventListener('keydown', function (e) {
526
+ if (e.key === 'ArrowDown') { e.preventDefault(); openReviewMenu(true); }
527
+ });
528
+ g.addEventListener('pointerenter', function (e) { if (byMouse(e)) openReviewMenu(false); });
529
+ g.addEventListener('pointerleave', function (e) {
530
+ if (!byMouse(e)) return;
531
+ clearTimeout(closeTimer);
532
+ closeTimer = setTimeout(function () { closeReviewMenu(false); }, 220);
533
+ });
534
+ g.appendChild(b);
535
+ g.appendChild(more);
536
+ g.appendChild(menu);
537
+ return g;
538
+ }
539
+
540
+ // The scribble wall: the top bar's empty space, doodled on while reviewing.
541
+ // craft-review.js draws it (decorative, aria-hidden); every motion is CSS,
542
+ // so the reduced-motion kill switch leaves three finished doodles standing.
543
+ var WALL = 'craft-graffiti';
544
+ var wallTool = { script: 'craft-review.js', api: 'OTBCraftReview' };
545
+ function applyWall(mode) {
546
+ var wall = document.getElementById(WALL);
547
+ var root = document.documentElement;
548
+ if (!review) {
549
+ remove(wall);
550
+ if (root.removeAttribute) root.removeAttribute('data-craft-review');
551
+ return;
552
+ }
553
+ if (root.setAttribute) root.setAttribute('data-craft-review', mode);
554
+ var side = document.querySelector && document.querySelector('#app-topbar > .topbar-side');
555
+ if (wall && wall.getAttribute('data-wall') === mode) return;
556
+ remove(wall);
557
+ if (!side || !side.parentNode) return;
558
+ wall = document.createElement('div');
559
+ wall.id = WALL;
560
+ wall.className = 'craft-graffiti';
561
+ wall.setAttribute('data-wall', mode);
562
+ wall.setAttribute('aria-hidden', 'true');
563
+ side.parentNode.insertBefore(wall, side);
564
+ loadTool(wallTool).then(function (api) {
565
+ var live = document.getElementById(WALL);
566
+ if (live === wall && review === mode) wall.innerHTML = api.wallHtml(mode);
567
+ }, function () { /* a bare wall; the menu still says you are reviewing */ });
323
568
  }
324
569
  // sessionStorage: this session's mode, or that the picker was already shown.
325
570
  var MODE_KEY = 'hall-craft';
@@ -371,13 +616,16 @@
371
616
  applyLook(mode);
372
617
  applyTallies(mode);
373
618
  applyTools(mode);
374
- try { document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: mode } })); } catch (_) { /* old engine */ }
619
+ applyWall(mode);
620
+ try { document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: mode, review: !!review } })); } catch (_) { /* old engine */ }
375
621
  }
376
622
 
377
623
  // Choose a mode: this session always; across sessions only when asked to keep it.
624
+ // Choosing any mode, Artistry included, ends a review (task 1004686).
378
625
  function choose(mode, keep) {
379
626
  mode = valid(mode);
380
627
  if (!mode) return;
628
+ if (review) { drop('sessionStorage', REVIEW_KEY); review = null; closeReviewMenu(false); }
381
629
  set('sessionStorage', MODE_KEY, mode);
382
630
  set('sessionStorage', ASKED_KEY, '1');
383
631
  if (keep === true) set('localStorage', KEEP_KEY, mode);
@@ -389,7 +637,8 @@
389
637
  // ---------- the header switch ----------
390
638
  function switchHtml() {
391
639
  if (pageOptsOut()) return '';
392
- var current = read();
640
+ review = storedReview();
641
+ var current = review || read();
393
642
  return '<div class="craft-switch' + (current ? '' : ' craft-switch--unset') + '" id="craft-switch" role="group" aria-label="Mode">' +
394
643
  MODES.map(function (m) {
395
644
  return '<button class="craft-switch__opt craft-switch__opt--' + m.id + '" type="button" data-craft-opt="' + m.id + '"' +
@@ -551,7 +800,8 @@
551
800
  var mounted = false;
552
801
  function mount() {
553
802
  if (pageOptsOut()) return;
554
- apply(read());
803
+ review = storedReview();
804
+ apply(wearing());
555
805
  if (mounted) return;
556
806
  mounted = true;
557
807
  var sw = document.getElementById('craft-switch');
@@ -572,8 +822,13 @@
572
822
 
573
823
  window.OTBCraftMode = {
574
824
  MODES: IDS.slice(),
575
- current: read,
825
+ current: wearing,
576
826
  choose: choose,
827
+ // Review mode (task 1004686): the experience under review, or null.
828
+ reviewing: function () { return review; },
829
+ review: startReview,
830
+ stopReview: stopReview,
831
+ EXPERIENCES: EXPERIENCES.map(function (x) { return x.id; }),
577
832
  open: openPicker,
578
833
  ALWAYS_TABS: ALWAYS_TABS.slice(),
579
834
  tabsFor: tabsFor,
@@ -0,0 +1,227 @@
1
+ /* craft-review.css — artist review mode (task 1004686; craft-mode.js draws the
2
+ menu, craft-review.js the wall). Imported by style.css, beside page-notes.css.
3
+
4
+ The Review button becomes a split pill: Review still opens the feedback doc,
5
+ and the chevron (or, where a pointer can hover, the whole pill) opens a menu
6
+ of the four experiences. While an artist reviews one, small black-and-white
7
+ doodles in the sky's hand are scribbled into the top bar's empty space.
8
+
9
+ Every colour is a token (tests/hall_audit.mjs holds that). Every motion is
10
+ CSS on transform and opacity, and the hall's reduced-motion kill switch
11
+ (style.css) leaves three doodles standing, finished, with no pen.
12
+
13
+ An @import lands BEFORE style.css's own rules, so a rule here that restyles
14
+ .craft-tool must out-rank it (two classes), or style.css silently wins. */
15
+
16
+ /* ---------- the split pill ---------- */
17
+ .craft-review {
18
+ position: relative;
19
+ display: inline-flex;
20
+ align-items: center;
21
+ flex-shrink: 0;
22
+ border-radius: var(--radius-pill);
23
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--craft, var(--ink)) 40%, transparent);
24
+ }
25
+ .craft-review > .craft-tool {
26
+ box-shadow: none;
27
+ border-radius: var(--radius-pill) 0 0 var(--radius-pill);
28
+ }
29
+ .craft-review__more {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ width: 24px;
34
+ height: 32px;
35
+ padding: 0 4px 0 0;
36
+ border: 0;
37
+ border-radius: 0 var(--radius-pill) var(--radius-pill) 0;
38
+ background: none;
39
+ color: var(--craft, var(--ink-soft));
40
+ cursor: pointer;
41
+ }
42
+ .craft-review__more svg { width: 12px; height: 12px; transition: transform 200ms cubic-bezier(.2, .7, .3, 1); }
43
+ .craft-review__more[aria-expanded="true"] svg { transform: rotate(180deg); }
44
+ .craft-review__more:hover { background: color-mix(in srgb, var(--craft, var(--ink)) 12%, transparent); }
45
+ .craft-review__more:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
46
+ @media (min-width: 1181px) {
47
+ .craft-review > .craft-tool { padding-right: 6px; }
48
+ }
49
+ @media (max-width: 1180px) {
50
+ .craft-review > .craft-tool { width: 30px; }
51
+ }
52
+
53
+ /* Reviewing: the pill takes a wash of the experience's colour, says
54
+ "Reviewing", and a drop of wet paint hangs from it. */
55
+ .craft-tool.craft-tool--reviewing { position: relative; background: color-mix(in srgb, var(--craft, var(--sea)) 14%, transparent); }
56
+ .craft-tool.craft-tool--reviewing::after {
57
+ content: '';
58
+ position: absolute;
59
+ left: 13px;
60
+ bottom: -4px;
61
+ width: 4px;
62
+ height: 7px;
63
+ border-radius: 0 0 3px 3px;
64
+ background: var(--craft, var(--sea));
65
+ }
66
+
67
+ /* ---------- the menu ---------- */
68
+ .craft-review__menu {
69
+ position: absolute;
70
+ top: calc(100% + 8px);
71
+ right: 0;
72
+ z-index: var(--z-drawer);
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 2px;
76
+ min-width: 248px;
77
+ padding: 6px;
78
+ box-sizing: border-box;
79
+ background: var(--bg-card);
80
+ border-radius: var(--radius);
81
+ box-shadow: 0 0 0 1px var(--glass-edge), var(--shadow-pop);
82
+ }
83
+ .craft-review__menu[hidden] { display: none; }
84
+ /* A bridge over the gap, so a pointer travelling down to the menu never
85
+ leaves the pill and the hover menu does not close under it. */
86
+ .craft-review__menu::before { content: ''; position: absolute; left: 0; right: 0; top: -8px; height: 8px; }
87
+ .craft-review__head {
88
+ margin: 8px 12px 2px;
89
+ font-family: var(--label-face);
90
+ font-size: 11px;
91
+ letter-spacing: .12em;
92
+ text-transform: uppercase;
93
+ color: var(--ink-faint);
94
+ }
95
+ .craft-review__item {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 10px;
99
+ min-height: 36px;
100
+ padding: 0 12px;
101
+ border: 0;
102
+ border-radius: var(--radius-sm);
103
+ background: none;
104
+ color: var(--ink);
105
+ font: inherit;
106
+ font-size: 14px;
107
+ text-align: left;
108
+ white-space: nowrap;
109
+ cursor: pointer;
110
+ }
111
+ .craft-review__item:hover { background: color-mix(in srgb, var(--craft, var(--ink)) 10%, transparent); }
112
+ .craft-review__item:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
113
+ .craft-review__icon { width: 16px; height: 16px; flex-shrink: 0; color: var(--ink-soft); }
114
+ .craft-review__item[aria-checked="true"] { color: var(--craft, var(--ink)); font-weight: 500; }
115
+ .craft-review__item[aria-checked="true"] .craft-review__icon { color: var(--craft, var(--ink)); }
116
+ .craft-review__check { width: 14px; height: 14px; margin-left: auto; flex-shrink: 0; }
117
+ .craft-review__item--stop { margin-top: 4px; border-top: 1px solid var(--rule-soft); border-radius: 0 0 var(--radius-sm) var(--radius-sm); }
118
+
119
+ /* A phone: no hover and no room for the chevron, so Review itself opens the
120
+ menu (craft-mode.js), and the menu spans the bar instead of hanging off a
121
+ pill that may sit near the left edge. */
122
+ @media (max-width: 640px) {
123
+ .craft-review { position: static; }
124
+ .craft-review__more { display: none; }
125
+ .craft-review > .craft-tool { width: 28px; border-radius: var(--radius-pill); }
126
+ .craft-review__menu { top: calc(100% + 6px); left: 12px; right: 12px; min-width: 0; }
127
+ .craft-review__item { min-height: 44px; }
128
+ }
129
+ /* Under 360px the bar holds two tools, not three: reviewing Ideating, its
130
+ own Ideas button folds away (craft-mode.js marks it a guest). */
131
+ @media (max-width: 359px) { .craft-tool.craft-tool--guest { display: none; } }
132
+
133
+ /* ---------- the scribble wall ---------- */
134
+ /* The top bar's empty space, between the title and the controls. Where none
135
+ is left (a phone), the Review pill still says "Reviewing". */
136
+ .craft-graffiti {
137
+ position: relative;
138
+ flex: 1 1 0;
139
+ min-width: 0;
140
+ align-self: stretch;
141
+ overflow: hidden;
142
+ pointer-events: none;
143
+ container: gx / inline-size;
144
+ }
145
+ @media (max-width: 640px) { .craft-graffiti { display: none; } }
146
+ /* Small walls keep the small doodles; a wall too tight for even one shows none. */
147
+ @container gx (max-width: 58px) { .gx-tag--wide { display: none; } }
148
+ @container gx (max-width: 34px) { .gx-tag { display: none; } }
149
+
150
+ /* Six doodles in the sky's hand (craft-review.js), each scribbled at its own
151
+ spot and height across the wall: a new one every five seconds of a
152
+ thirty-second round, each staying about ten, so two or three are up at once. */
153
+ .gx-tag {
154
+ position: absolute;
155
+ top: calc(50% - 12px + var(--gx-dy, 0px));
156
+ width: var(--gx-w);
157
+ height: 24px;
158
+ --gx-delay: 0s;
159
+ }
160
+ .gx-tag--1 { left: calc((100% - var(--gx-w)) * .06); --gx-dy: -7px; }
161
+ .gx-tag--2 { left: calc((100% - var(--gx-w)) * .55); --gx-dy: 6px; --gx-delay: 5s; }
162
+ .gx-tag--3 { left: calc((100% - var(--gx-w)) * .3); --gx-dy: 4px; --gx-delay: 10s; }
163
+ .gx-tag--4 { left: calc((100% - var(--gx-w)) * .84); --gx-dy: -6px; --gx-delay: 15s; }
164
+ .gx-tag--5 { left: calc((100% - var(--gx-w)) * .42); --gx-dy: -5px; --gx-delay: 20s; }
165
+ .gx-tag--6 { left: calc((100% - var(--gx-w)) * .72); --gx-dy: 7px; --gx-delay: 25s; }
166
+
167
+ /* The wet line: the window slides in from the left while the drawing inside
168
+ it slides the other way by the same amount, so the doodle stays put and is
169
+ uncovered left to right. The pen tip rides that edge, jittering up and
170
+ down as it scribbles. */
171
+ .gx-win, .gx-track { position: absolute; inset: -2px -3px; }
172
+ .gx-win { overflow: hidden; }
173
+ .gx-ink { position: absolute; inset: 0; padding: 2px 3px; }
174
+ .gx-svg { display: block; overflow: visible; }
175
+ .gx-win, .gx-ink, .gx-track {
176
+ animation-duration: 30s;
177
+ animation-timing-function: linear;
178
+ animation-iteration-count: infinite;
179
+ animation-fill-mode: both;
180
+ animation-delay: var(--gx-delay);
181
+ }
182
+ .gx-win { animation-name: gx-win; }
183
+ .gx-ink { animation-name: gx-ink; }
184
+ .gx-track { animation-name: gx-track; opacity: 0; }
185
+ /* The rest frame (and the reduced-motion one): three doodles, finished. */
186
+ .gx-tag:not(.gx-tag--1):not(.gx-tag--2):not(.gx-tag--4) .gx-win { opacity: 0; }
187
+
188
+ @keyframes gx-win {
189
+ 0% { transform: translateX(-100%); opacity: 1; }
190
+ 4% { transform: translateX(0); }
191
+ 28% { opacity: 1; }
192
+ 32%, 100% { transform: translateX(0); opacity: 0; }
193
+ }
194
+ @keyframes gx-ink {
195
+ 0% { transform: translateX(100%); }
196
+ 4%, 100% { transform: translateX(0); }
197
+ }
198
+ /* The pen waits unseen: a doodle not yet begun shows no tip. */
199
+ @keyframes gx-track {
200
+ 0% { transform: translateX(-100%); opacity: 0; }
201
+ .3% { opacity: 1; }
202
+ 4% { transform: translateX(0); opacity: 1; }
203
+ 5%, 100% { transform: translateX(0); opacity: 0; }
204
+ }
205
+
206
+ .gx-nib {
207
+ position: absolute;
208
+ right: -1.5px;
209
+ top: calc(50% - 1.5px);
210
+ width: 3px;
211
+ height: 3px;
212
+ border-radius: 999px;
213
+ background: var(--ink);
214
+ animation: gx-scribble .16s ease-in-out infinite alternate;
215
+ }
216
+ @keyframes gx-scribble {
217
+ from { transform: translateY(-7px); }
218
+ to { transform: translateY(7px); }
219
+ }
220
+
221
+ /* The ink: hairlines and dots, black on the light bar and white on the dark
222
+ one, with the sky's own ladder for the fainter marks. */
223
+ .gx-ln { fill: none; stroke: var(--ink); stroke-width: 1.3px; stroke-linecap: round; stroke-linejoin: round; }
224
+ .gx-ln--soft { stroke: color-mix(in srgb, var(--ink) 55%, transparent); }
225
+ .gx-ln--dash { stroke: color-mix(in srgb, var(--ink) 55%, transparent); stroke-width: 1px; stroke-dasharray: 2 2.5; }
226
+ .gx-pt { fill: var(--ink); }
227
+ .gx-halo { fill: none; stroke: color-mix(in srgb, var(--ink) 28%, transparent); stroke-width: 1px; }