@bongos/core 1.20.76 → 1.20.78

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.
Files changed (62) hide show
  1. package/.bongos-core.json +93 -63
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +3 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0136-update-channel-subscription-policy.md +1 -1
  8. package/docs/adr/0360-bongos-follows-every-candidate-every-other-project-follows-releases.md +66 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +70 -3
  11. package/docs/api-reference.md +3 -2
  12. package/docs/copy-inventory.md +531 -458
  13. package/docs/copy-registry.json +1269 -589
  14. package/docs/module-api-changelog.md +4 -0
  15. package/docs/page-inventory.json +11 -1
  16. package/docs/page-readings.json +1208 -1082
  17. package/docs/recipes/upgrading-the-core.md +11 -2
  18. package/modules/copy-desk/artist-stats.js +15 -1
  19. package/modules/copy-desk/module.json +1 -1
  20. package/modules/copy-desk/page-status.js +41 -0
  21. package/modules/copy-desk/routes/copy-desk-artist.js +44 -0
  22. package/modules/copy-desk/tests/copy_no_cms.mjs +11 -2
  23. package/modules/hall-ui/public/deploy.states.json +1 -1
  24. package/modules/hall-ui/public/profile-crafts.js +231 -0
  25. package/modules/hall-ui/public/profile.css +60 -0
  26. package/modules/hall-ui/public/profile.html +31 -12
  27. package/modules/hall-ui/public/profile.js +180 -75
  28. package/modules/hall-ui/public/profile.states.json +34 -0
  29. package/modules/hall-ui/public/sky-draw.js +66 -39
  30. package/modules/hall-ui/public/sky-interaction.js +1 -1
  31. package/modules/hall-ui/public/sky-panel.js +1 -1
  32. package/modules/hall-ui/public/thinking.css +52 -24
  33. package/modules/hall-ui/records/profile-roles.md +18 -0
  34. package/modules/npm-release/public/work.js +72 -0
  35. package/modules/npm-release/work.js +67 -1
  36. package/modules/provisioning/planet-physics.js +8 -0
  37. package/modules/provisioning/soft-limits.js +9 -0
  38. package/modules/public-landing/public/projects.html +579 -37
  39. package/modules/public-landing/public/projects.probes.json +6 -3
  40. package/modules/public-landing/public/projects.states.json +20 -14
  41. package/modules/ui-design/kit/serve.js +26 -3
  42. package/package-lock.json +2 -2
  43. package/package.json +1 -1
  44. package/release-notes.json +24 -0
  45. package/scripts/gds/update-channel.js +68 -1
  46. package/scripts/gds/update-sweep.js +100 -4
  47. package/scripts/migrate.sh +6 -2
  48. package/src/module-api.js +1 -1
  49. package/tests/hall_sky.mjs +77 -0
  50. package/tests/helpers.mjs +32 -7
  51. package/tests/migrate_applied_check.mjs +94 -0
  52. package/tests/npm_release_page.mjs +67 -0
  53. package/tests/npm_release_work.mjs +82 -0
  54. package/tests/profile_activity.mjs +23 -15
  55. package/tests/profile_role_headline.mjs +4 -4
  56. package/tests/profile_role_tabs.mjs +163 -0
  57. package/tests/projects_hub_app_step.mjs +4 -1
  58. package/tests/provisioning_planet_physics.mjs +24 -2
  59. package/tests/update_channel.mjs +80 -1
  60. package/tests/update_subscription_engine.mjs +99 -0
  61. package/tests/wizard_physics.mjs +11 -4
  62. package/tests/wizard_physics_more.mjs +470 -0
@@ -215,6 +215,60 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
215
215
  .physCallout{color:var(--ink);text-transform:none;letter-spacing:0;font-family:var(--font-body);font-size:13px;}
216
216
  .physCoords{color:var(--ink-faint);}
217
217
  @media (max-width:840px){ .physRow2{grid-template-columns:1fr;} }
218
+ /* questions 5 to 7 and the summary (task 1004419, BV2.PS11). The rail strikes a question
219
+ the answers skip; the options are the wizard's hairline pills in one column, the suite a
220
+ two-column grid; the growth guess is a ledger of three months over a range. */
221
+ .physRail li.skipped,#view-new .steps li.skipped .slbl{text-decoration:line-through;text-decoration-color:var(--ink-faint);}
222
+ .typeCards.physOpts{grid-template-columns:1fr;}
223
+ .physOpts .tpill,.physSuiteGrid .tpill{align-items:flex-start;border-left:0;padding-left:0;border-top:1px solid var(--rule-soft);}
224
+ .physOpts .tpill:first-child{border-top:0;}
225
+ .physOpts .tpill::before,.physSuiteGrid .tpill::before{margin-top:6px;}
226
+ .physSuite{margin:4px 0 12px;padding:0;border:0;}
227
+ .physSuite legend{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);margin:0 0 6px;padding:0;}
228
+ .typeCards.physSuiteGrid{grid-template-columns:repeat(2,minmax(0,1fr));gap:0 16px;}
229
+ .physSuiteGrid .tpill:nth-child(-n+2){border-top:0;}
230
+ .physWhyList{margin:0 0 14px;padding:0 0 0 18px;font-size:14px;line-height:1.5;color:var(--ink-soft);max-width:62ch;}
231
+ .physGuess{margin:12px 0 10px;padding:14px 0 0;border-top:1px solid var(--rule);max-width:62ch;}
232
+ .physGuessLine{font-size:15px;line-height:1.5;color:var(--ink);margin:0 0 12px;}
233
+ .physMonths{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));grid-template-rows:auto auto;grid-auto-flow:column;gap:2px 16px;margin:0 0 6px;padding-top:10px;border-top:1px solid var(--rule-soft);}
234
+ .physMonths dt{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
235
+ .physMonths dd{margin:0;font-family:var(--font-display);font-weight:300;font-size:30px;line-height:1.1;color:var(--ink);font-variant-numeric:tabular-nums;}
236
+ .physAdjust input[type=range]{display:block;width:100%;min-height:24px;margin:6px 0 0;accent-color:var(--accent);background:none;}
237
+ .physLedger dd{display:flex;align-items:flex-start;gap:12px;}
238
+ .physLedger dd>span{flex:1;min-width:0;}
239
+ .physLedger .physLink{flex:none;}
240
+ @media (max-width:520px){ .typeCards.physSuiteGrid{grid-template-columns:1fr;} .physSuiteGrid .tpill:nth-child(2){border-top:1px solid var(--rule-soft);} }
241
+ /* the stage's new bodies: hairline rings, beads for moons, the platform's mark as the
242
+ sun the planet orbits — nearer the more the platform runs, absent for its own system */
243
+ .physBelt{position:absolute;left:50%;top:50%;border:1px solid var(--rule);border-radius:50%;transform:translate(-50%,-50%) rotate(-16deg);}
244
+ .physBelt[data-ring="1"]{width:76%;height:22%;}
245
+ .physBelt[data-ring="2"]{width:84%;height:25%;}
246
+ .physBelt[data-ring="3"]{width:92%;height:28%;border-color:var(--rule-soft);}
247
+ .physBelt[data-ring="4"]{width:98%;height:31%;border-color:var(--rule-soft);}
248
+ .physMoons,.physGhosts{position:absolute;border-radius:50%;}
249
+ .physMoons{inset:20%;}
250
+ .physGhosts{inset:9%;}
251
+ .physMoons img,.physGhosts img{position:absolute;width:12%;height:auto;transform:translate(-50%,-50%);}
252
+ .physGhosts img{width:8%;opacity:.45;}
253
+ /* the first places taken are the ones clear of the caption under the planet */
254
+ .physMoons img:nth-child(1),.physGhosts img:nth-child(1){left:50%;top:0;}
255
+ .physMoons img:nth-child(2),.physGhosts img:nth-child(2){left:0;top:50%;}
256
+ .physMoons img:nth-child(3),.physGhosts img:nth-child(3){left:100%;top:50%;}
257
+ .physMoons img:nth-child(4),.physGhosts img:nth-child(4){left:85.4%;top:14.6%;}
258
+ .physMoons img:nth-child(5){left:14.6%;top:14.6%;}
259
+ .physMoons img:nth-child(6){left:50%;top:100%;}
260
+ .physStage .physCap,.physStage .physFoot{z-index:2;}
261
+ .physStage .physCap,.physStage .physCallout,.physStage .physCoords{text-shadow:var(--type-halo);}
262
+ .physSun{position:absolute;width:15%;height:auto;left:var(--sun-x);top:var(--sun-y);transform:translate(-50%,-50%);}
263
+ .physTether{position:absolute;left:50%;top:50%;width:var(--sun-d);height:0;border-top:1px dashed var(--rule);transform-origin:0 0;transform:rotate(-45deg);}
264
+ #physStage[data-orbit="near"]{--sun-x:73%;--sun-y:27%;--sun-d:33%;}
265
+ #physStage[data-orbit="mid"]{--sun-x:80%;--sun-y:20%;--sun-d:42%;}
266
+ #physStage[data-orbit="far"]{--sun-x:87%;--sun-y:13%;--sun-d:52%;}
267
+ @media (prefers-reduced-motion:no-preference){
268
+ .physMoons{animation:physOrbit 160s linear infinite;}
269
+ .physGhosts{animation:physOrbit 260s linear infinite reverse;}
270
+ @keyframes physOrbit{to{transform:rotate(360deg);}}
271
+ }
218
272
  /* under 520 the two doors cannot sit side by side at 48px, so they stack —
219
273
  still two equal doors in one object */
220
274
  @media (max-width:520px){
@@ -1486,8 +1540,8 @@ summary{min-height:24px;padding:3px 0;}
1486
1540
  Autofill, State, StateExisting, NotPossible, Geography, Ages, Entity and
1487
1541
  EntityExisting frames, docs/design/project-startup-direction.md). ONE rail step
1488
1542
  with its own inner sequence, so the wizard's numbering moved by one and the
1489
- canvas's seven-question rail lives inside the panel (questions 5 to 7 join it in
1490
- BV2.PS11). Left: the question, its choices and the doors. Right: the field plate
1543
+ canvas's seven-question rail lives inside the panel (questions 5 to 7 and the
1544
+ summary joined it in BV2.PS11, task 1004419). Left: the question, its choices and the doors. Right: the field plate
1491
1545
  with the planet forming from the world's own plates as the answers come in.
1492
1546
  Every answer rides the create request's `detail` (planet-physics.js); the stop
1493
1547
  screen and the soft-limit table come from POST /provisioning/physics-check, the
@@ -1496,9 +1550,12 @@ summary{min-height:24px;padding:3px 0;}
1496
1550
  <section class="panel" id="panel-physics" aria-labelledby="phys-h0">
1497
1551
  <div class="forkBody physBody">
1498
1552
  <div>
1499
- <p class="physCrumb">planet creation · part 1 of 2 · planet physics</p>
1553
+ <!-- the voice (spec D5, task 1004419): a formed organization reads the plain
1554
+ professional copy, everyone else the planet's. Both variants are baked;
1555
+ physPaintVoice() shows one. -->
1556
+ <p class="physCrumb"><span data-voice="planet">planet creation · part 1 of 2 · planet physics</span><span data-voice="pro" hidden>project setup · part 1 of 2 · project profile</span></p>
1500
1557
  <ol class="physRail" id="physRail" aria-label="Planet physics questions">
1501
- <li data-q="1">state</li><li data-q="2">geography</li><li data-q="3">ages</li><li data-q="4">entity</li><li>builders</li><li>rewards</li><li>framing</li>
1558
+ <li data-q="1">state</li><li data-q="2">geography</li><li data-q="3">ages</li><li data-q="4">entity</li><li data-q="5"><span data-voice="planet">builders</span><span data-voice="pro" hidden>team</span></li><li data-q="6">rewards</li><li data-q="7">framing</li>
1502
1559
  </ol>
1503
1560
  <form data-next="4" id="physForm">
1504
1561
  <div class="physQ" data-q="0">
@@ -1598,6 +1655,85 @@ summary{min-height:24px;padding:3px 0;}
1598
1655
  </div>
1599
1656
  </div>
1600
1657
 
1658
+ <!-- questions 5 to 7 and the summary (task 1004419, BV2.PS11; the canvas's
1659
+ Builders, BuildersPro, Rewards, Framing and Physics frames). Further
1660
+ screens of this same step, so the wizard is not renumbered. -->
1661
+ <div class="physQ" data-q="5" hidden>
1662
+ <h1 id="phys-h5"><span data-voice="planet">Who will live <em>on your planet?</em></span><span data-voice="pro" hidden>How many people <em>will work on this?</em></span></h1>
1663
+ <p class="lede"><span data-voice="planet">One builder, or many? This can always change.</span><span data-voice="pro" hidden>One person or a team. You can change this at any time.</span></p>
1664
+ <div class="typeCards physCells" role="group" aria-label="One or many">
1665
+ <button type="button" class="tpill" data-phys-solo="one" aria-pressed="false"><span><span data-voice="planet">One</span><span data-voice="pro" hidden>One person</span> <small><span data-voice="planet">Just me, building solo.</span><span data-voice="pro" hidden>A single contributor.</span></small></span></button>
1666
+ <button type="button" class="tpill" data-phys-solo="many" aria-pressed="false"><span><span data-voice="planet">Many</span><span data-voice="pro" hidden>A team</span> <small><span data-voice="planet">A team, now or soon.</span><span data-voice="pro" hidden>Several contributors, now or soon.</span></small></span></button>
1667
+ </div>
1668
+ <div id="physTeam" hidden>
1669
+ <div class="wizField"><span id="physStartLbl"><span data-voice="planet">Builders at the start, you included</span><span data-voice="pro" hidden>Contributors at the start</span></span>
1670
+ <div class="demoStep" role="group" aria-labelledby="physStartLbl">
1671
+ <button type="button" class="btn small ghost" data-phys-count="-1" aria-label="One fewer">−</button>
1672
+ <output id="physStartN" aria-live="polite">2</output>
1673
+ <button type="button" class="btn small ghost" data-phys-count="1" aria-label="One more">+</button>
1674
+ </div>
1675
+ </div>
1676
+ <div class="physGuess">
1677
+ <p class="physNoteH"><span data-voice="planet">A growth guess</span><span data-voice="pro" hidden>Our estimate</span></p>
1678
+ <p class="physGuessLine" id="physGuessLine" aria-live="polite"></p>
1679
+ <dl class="physMonths">
1680
+ <dt>Month 1</dt><dd id="physM1">2</dd>
1681
+ <dt>Month 2</dt><dd id="physM2">4</dd>
1682
+ <dt>Month 3</dt><dd id="physM3">5</dd>
1683
+ </dl>
1684
+ <label class="wizField physAdjust"><span>Adjust month three</span>
1685
+ <input type="range" id="physExpected" min="2" max="30" step="1" value="5">
1686
+ </label>
1687
+ </div>
1688
+ </div>
1689
+ </div>
1690
+
1691
+ <div class="physQ" data-q="6" hidden>
1692
+ <h1 id="phys-h6"><span data-voice="planet">How will builders <em>be rewarded?</em></span><span data-voice="pro" hidden>How will contributors <em>be rewarded?</em></span></h1>
1693
+ <p class="lede"><span data-voice="planet">Pick everything you’re considering. This is intent only: you’ll design the binding rewards later, once your entity is settled.</span><span data-voice="pro" hidden>Select every option you are considering. Nothing here is binding: the reward terms are set later, once the entity is settled.</span></p>
1694
+ <div class="typeCards physCells physOpts" role="group" aria-label="Rewards being considered">
1695
+ <button type="button" class="tpill" data-phys="reward_intents" data-val="non-monetary" aria-pressed="false"><span>Non-monetary <small>Entertainment, learning, experience, volunteering.</small></span></button>
1696
+ <button type="button" class="tpill" data-phys="reward_intents" data-val="cash" aria-pressed="false"><span>Cash for work <small>Paid in money for the work done.</small></span></button>
1697
+ <button type="button" class="tpill" data-phys="reward_intents" data-val="equity" aria-pressed="false"><span>Equity or shares <small>A stake in the entity.</small></span></button>
1698
+ <button type="button" class="tpill" data-phys="reward_intents" data-val="future-payouts" aria-pressed="false"><span>Future profits or payouts <small>Paid later, tied to conditions you set.</small></span></button>
1699
+ </div>
1700
+ <ul class="physWhyList" data-why-list="reward_intents" hidden></ul>
1701
+ </div>
1702
+
1703
+ <div class="physQ" data-q="7" hidden>
1704
+ <h1 id="phys-h7">How much should <em><span data-brand-product-inline>Cloud Bongos</span> run for you?</em></h1>
1705
+ <p class="lede">Not binding. It just speeds up your setup.</p>
1706
+ <div class="typeCards physCells physOpts" role="group" aria-label="How much is run for you">
1707
+ <button type="button" class="tpill" data-phys="framing" data-val="managed" aria-pressed="false"><span>A · All managed <small>The full suite, run for you.</small></span></button>
1708
+ <button type="button" class="tpill" data-phys="framing" data-val="hybrid" aria-pressed="false"><span>B · Hybrid <small>Choose what you want from the full suite.</small></span></button>
1709
+ <button type="button" class="tpill" data-phys="framing" data-val="local" aria-pressed="false"><span>C · Local or internal <small>Choose from the suite, and keep your data on your own machines.</small></span></button>
1710
+ <button type="button" class="tpill" data-phys="framing" data-val="open-source" aria-pressed="false"><span>D · Open source, local <small>Just the free core. Build on your own machines and pay only for what you add.</small></span></button>
1711
+ </div>
1712
+ <p class="physWhy" data-why="services" hidden></p>
1713
+ <fieldset class="physSuite" id="physSuite" hidden>
1714
+ <legend id="physSuiteLbl">Your hybrid suite</legend>
1715
+ <div class="typeCards physCells physSuiteGrid">
1716
+ <button type="button" class="tpill" data-phys="services" data-val="hosting" aria-pressed="false">Hosting</button>
1717
+ <button type="button" class="tpill" data-phys="services" data-val="labor-payment" aria-pressed="false">Labor payment and HR</button>
1718
+ <button type="button" class="tpill" data-phys="services" data-val="starter-kit" aria-pressed="false">Starting module kit</button>
1719
+ <button type="button" class="tpill" data-phys="services" data-val="marketplace-buy" aria-pressed="false">Module marketplace: buy</button>
1720
+ <button type="button" class="tpill" data-phys="services" data-val="marketplace-sell" aria-pressed="false">Module marketplace: sell</button>
1721
+ <button type="button" class="tpill" data-phys="services" data-val="guild-access" aria-pressed="false">Builder guild access</button>
1722
+ <button type="button" class="tpill" data-phys="services" data-val="guild-create" aria-pressed="false">Create a builder guild</button>
1723
+ <button type="button" class="tpill" data-phys="services" data-val="company-creation" aria-pressed="false">Company creation</button>
1724
+ <button type="button" class="tpill" data-phys="services" data-val="cbaas-consulting" aria-pressed="false">Expert consulting</button>
1725
+ <button type="button" class="tpill" data-phys="services" data-val="public-marketing" aria-pressed="false">Public marketed project</button>
1726
+ </div>
1727
+ <ul class="physWhyList" data-why-list="services" hidden></ul>
1728
+ </fieldset>
1729
+ </div>
1730
+
1731
+ <div class="physQ" data-q="sum" hidden>
1732
+ <h1 id="phys-hsum"><span data-voice="planet">Your planet’s <em>physics</em></span><span data-voice="pro" hidden>Your project’s <em>profile</em></span></h1>
1733
+ <p class="lede"><span data-voice="planet">This is what shapes your planet. Change any answer before it’s born.</span><span data-voice="pro" hidden>This is what shapes your project. Change any answer before it is created.</span></p>
1734
+ <dl class="demoLedger physLedger" id="physSummary"></dl>
1735
+ </div>
1736
+
1601
1737
  <div class="physQ" data-q="stop" hidden>
1602
1738
  <h1 id="phys-hstop">This can’t be built <em>on <span data-brand-product-inline>Cloud Bongos</span></em></h1>
1603
1739
  <p class="lede" id="physStopMsg"></p>
@@ -1617,8 +1753,16 @@ summary{min-height:24px;padding:3px 0;}
1617
1753
  <div class="forkStage physStage" id="physStage" aria-hidden="true">
1618
1754
  <span class="forkOrbit"></span>
1619
1755
  <span class="forkOrbit forkOrbitDash"></span>
1756
+ <!-- task 1004419: rings for the rewards being considered, moons for the
1757
+ builders (the faint ones on the dashed orbit are the expected growth), and
1758
+ the planet's orbit around the platform's mark, set by the framing -->
1759
+ <span class="physBelt" data-ring="1" hidden></span><span class="physBelt" data-ring="2" hidden></span><span class="physBelt" data-ring="3" hidden></span><span class="physBelt" data-ring="4" hidden></span>
1760
+ <span class="physGhosts" id="physGhosts" hidden><img src="/assets/world/r3-frost-bead-cut.webp" alt=""><img src="/assets/world/r3-frost-bead-cut.webp" alt=""><img src="/assets/world/r3-frost-bead-cut.webp" alt=""><img src="/assets/world/r3-frost-bead-cut.webp" alt=""></span>
1761
+ <span class="physMoons" id="physMoons" hidden><img src="/assets/world/r3-frost-bead-cut.webp" alt=""><img src="/assets/world/r3-frost-bead-cut.webp" alt=""><img src="/assets/world/r3-frost-bead-cut.webp" alt=""><img src="/assets/world/r3-frost-bead-cut.webp" alt=""><img src="/assets/world/r3-frost-bead-cut.webp" alt=""><img src="/assets/world/r3-frost-bead-cut.webp" alt=""></span>
1762
+ <span class="physTether" id="physTether" hidden></span>
1620
1763
  <span class="physGuard" id="physGuard" hidden></span>
1621
1764
  <img class="forkSeed physPlanet" id="physPlanet" src="/assets/world/r2-chrome-blob-one-cut.webp" alt="">
1765
+ <img class="physSun" id="physSun" src="/assets/world/r2-bongos-chrome-a-cut.webp" alt="" hidden>
1622
1766
  <p class="physCap"><span id="physCapLbl">planet physics</span> · <span id="physCapN">0 of 7</span></p>
1623
1767
  <div class="physFoot"><p class="physCallout" id="physCallout">physics · the rules it lives by · each answer shapes your planet here</p>
1624
1768
  <p class="physCoords" id="physCoords"></p></div>
@@ -1667,6 +1811,8 @@ summary{min-height:24px;padding:3px 0;}
1667
1811
  <b id="typeCardName"></b>
1668
1812
  <p id="typeCardDesc"></p>
1669
1813
  </div>
1814
+ <!-- pre-filled from the planet-physics answers (task 1004419), and says so -->
1815
+ <p class="physWhy" id="typePrefill" hidden></p>
1670
1816
  <div class="hint" id="h-projtype">&nbsp;</div>
1671
1817
  <div class="nav">
1672
1818
  <button type="button" class="btn ghost" data-back="4">← Back</button>
@@ -1767,6 +1913,8 @@ summary{min-height:24px;padding:3px 0;}
1767
1913
  <p>Layer <span data-brand-product-inline>Cloud Bongos</span> onto an existing repository — history, code, and backlog intact. Public or private, we never change it.</p>
1768
1914
  </button>
1769
1915
  </div>
1916
+ <!-- pre-filled from the planet-physics answers (task 1004419), and says so -->
1917
+ <p class="physWhy" id="homePrefill" hidden></p>
1770
1918
  <div class="reassure" id="adoptNote">Adopting never overwrites your files: you get a conflict pre-flight first, nothing is written until you accept, and your existing backlog (issues, TODOs) is imported as real tasks.</div>
1771
1919
 
1772
1920
  <!-- GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099.
@@ -4850,6 +4998,8 @@ summary{min-height:24px;padding:3px 0;}
4850
4998
  var now = !landed && n === cur;
4851
4999
  li.classList.toggle('now', now);
4852
5000
  li.classList.toggle('done', landed || n < cur);
5001
+ /* a step the planet-physics answers skip (task 1004419); still reachable */
5002
+ li.classList.toggle('skipped', step < 13 && physStepSkipped(n));
4853
5003
  if (now) li.setAttribute('aria-current', 'step'); else li.removeAttribute('aria-current');
4854
5004
  /* a step already answered is a way BACK (task 1003246): only those take
4855
5005
  the button role, the tab stop and the data-goto the delegated handler
@@ -4891,6 +5041,7 @@ summary{min-height:24px;padding:3px 0;}
4891
5041
  /* the picker repaints on entry, not once at boot: the owner can walk back
4892
5042
  to the Type step and change the answer the preselect is keyed to */
4893
5043
  if (step === 3) paintPhysics();
5044
+ if (step === 5 || step === 8) physPaintPrefillNotes();
4894
5045
  if (step === 7) renderModules();
4895
5046
  if (step === 8) { syncHome(); if (authKnown) ensureRepoAccess(); }
4896
5047
  /* a resumed draft can land PAST the Home step with the probe never run —
@@ -5069,6 +5220,7 @@ summary{min-height:24px;padding:3px 0;}
5069
5220
  $('adoptNote').classList.toggle('on', mode === 'adopt');
5070
5221
  saveDraft();
5071
5222
  syncHome();
5223
+ physPaintPrefillNotes();
5072
5224
  }
5073
5225
  $('c-green').addEventListener('click', function () { setMode('greenfield'); });
5074
5226
  $('c-adopt').addEventListener('click', function () { setMode('adopt'); });
@@ -5096,6 +5248,7 @@ summary{min-height:24px;padding:3px 0;}
5096
5248
  state.projType = b.getAttribute('data-type');
5097
5249
  $('h-projtype').innerHTML = '&nbsp;'; $('h-projtype').className = 'hint';
5098
5250
  paintType(state.projType);
5251
+ physPaintPrefillNotes();
5099
5252
  saveDraft();
5100
5253
  });
5101
5254
  });
@@ -5605,12 +5758,13 @@ summary{min-height:24px;padding:3px 0;}
5605
5758
  saveDraft();
5606
5759
  });
5607
5760
 
5608
- /* ── planet physics, questions 1 to 4 (task 1004418, BV2.PS07) ──
5761
+ /* ── planet physics, questions 1 to 7 (tasks 1004418 and 1004419, BV2.PS07 + PS11) ──
5609
5762
  One wizard step with its own inner sequence: 0 account-or-scratch, 1 state,
5610
- 2 geography, 3 ages, 4 entity, and the stop screen. The answers live in
5611
- state.physics.answers under planet-physics.js's own keys, so the create sends
5612
- them as `detail` untouched. The vocabularies below are COPIES of that file's
5613
- (a page cannot require a module), held equal by tests/wizard_physics.mjs. */
5763
+ 2 geography, 3 ages, 4 entity, 5 builders, 6 rewards, 7 framing, the summary
5764
+ ('sum') and the stop screen. The answers live in state.physics.answers under
5765
+ planet-physics.js's own keys, so the create sends them as `detail` untouched.
5766
+ The vocabularies below are COPIES of that file's (a page cannot require a
5767
+ module), held equal by tests/wizard_physics.mjs and wizard_physics_more.mjs. */
5614
5768
  var PHYS_COUNTRIES = ('AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ ' +
5615
5769
  'BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM ' +
5616
5770
  'DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS ' +
@@ -5649,7 +5803,43 @@ summary{min-height:24px;padding:3px 0;}
5649
5803
  };
5650
5804
  var PHYS_CORE_WORD = { frost: 'individual', chrome: 'group', gold: 'organization', obsidian: 'nonprofit' };
5651
5805
  var PHYS_GEO_WORD = { 'us-only': 'us only', 'us-eu': 'us + eu', 'global': 'global', 'not-sure': 'not sure yet' };
5652
- var PHYS_LAST = 4;
5806
+ /* Questions 5 to 7 (task 1004419). The voice (spec D5): a formed organization
5807
+ reads the plain professional copy; individuals, groups and every "not yet" read
5808
+ the planet's. The lists are planet-physics.js's, in its order. */
5809
+ var PHYS_ORGS = ['nonprofit', 'for-profit', 'corporate-division', 'research'];
5810
+ var PHYS_REWARDS = ['non-monetary', 'cash', 'equity', 'future-payouts'];
5811
+ var PHYS_FRAMINGS = ['managed', 'hybrid', 'local', 'open-source'];
5812
+ var PHYS_SERVICES = ['hosting', 'labor-payment', 'starter-kit', 'marketplace-buy', 'marketplace-sell', 'guild-access', 'guild-create', 'company-creation', 'cbaas-consulting', 'public-marketing'];
5813
+ var PHYS_MANY = ['reward_intents', 'services'];
5814
+ /* the suite a hybrid or local framing starts from (the canvas's Framing frame) */
5815
+ var PHYS_SUITE_START = { hybrid: ['hosting', 'starter-kit', 'marketplace-buy', 'guild-access'], local: ['starter-kit', 'marketplace-buy', 'guild-access'] };
5816
+ /* the framing is the planet's orbit around the platform: all managed is close,
5817
+ hybrid the middle, local far, and open source a system of its own */
5818
+ var PHYS_ORBIT = { managed: 'near', hybrid: 'mid', local: 'far', 'open-source': 'own' };
5819
+ var PHYS_ORBIT_WORD = { managed: 'a close orbit around the platform', hybrid: 'a middle orbit around the platform', local: 'a far orbit around the platform', 'open-source': 'a system of its own' };
5820
+ /* what the summary says for each answer */
5821
+ var PHYS_SAY = {
5822
+ project_state: { 'new': 'New', existing: 'Existing' },
5823
+ builder_geography: { 'us-only': 'Builders in the US only', 'us-eu': 'Builders in the US and EU', global: 'Builders anywhere', 'not-sure': 'Not sure yet where builders will be' },
5824
+ entity: {
5825
+ 'intent-company': 'Probably a company', 'intent-nonprofit': 'Probably a nonprofit', 'intent-solo': 'Just me for now', 'intent-unsure': 'Don’t know yet',
5826
+ nonprofit: 'Nonprofit', 'for-profit': 'For-profit company', 'corporate-division': 'Division of a corporation', research: 'Research entity',
5827
+ 'university-project': 'University project', 'social-group': 'Social group', 'single-owner-llc': 'Single-owner LLC', 'private-individual': 'Private individual'
5828
+ },
5829
+ reward_intents: { 'non-monetary': 'Non-monetary', cash: 'Cash for work', equity: 'Equity or shares', 'future-payouts': 'Future profits or payouts' },
5830
+ framing: { managed: 'All managed', hybrid: 'Hybrid', local: 'Local or internal', 'open-source': 'Open source, local' },
5831
+ services: {
5832
+ hosting: 'Hosting', 'labor-payment': 'Labor payment and HR', 'starter-kit': 'Starting module kit', 'marketplace-buy': 'Module marketplace: buy',
5833
+ 'marketplace-sell': 'Module marketplace: sell', 'guild-access': 'Builder guild access', 'guild-create': 'Create a builder guild',
5834
+ 'company-creation': 'Company creation', 'cbaas-consulting': 'Expert consulting', 'public-marketing': 'Public marketed project'
5835
+ }
5836
+ };
5837
+ /* the project type the entity suggests for the Type step (a pre-fill the founder sees and can change) */
5838
+ var PHYS_TYPE = {
5839
+ 'intent-company': 'business', 'for-profit': 'business', 'corporate-division': 'business', 'single-owner-llc': 'business',
5840
+ 'intent-nonprofit': 'non-profit', nonprofit: 'non-profit', research: 'research', 'university-project': 'research'
5841
+ };
5842
+ var PHYS_LAST = 7;
5653
5843
  var physLimits = [];
5654
5844
 
5655
5845
  function phys() {
@@ -5661,15 +5851,122 @@ summary{min-height:24px;padding:3px 0;}
5661
5851
  work only for an existing planet (the soft-limit table hides it for a new one). */
5662
5852
  function physDetail() {
5663
5853
  var a = phys().answers, out = {};
5664
- ['project_state', 'home_country', 'home_region', 'builder_geography', 'minors_on_board', 'entity'].forEach(function (k) {
5854
+ ['project_state', 'home_country', 'home_region', 'builder_geography', 'minors_on_board', 'entity',
5855
+ 'builders_at_start', 'builders_expected', 'framing'].forEach(function (k) {
5665
5856
  if (a[k] !== undefined && a[k] !== null && a[k] !== '') out[k] = a[k];
5666
5857
  });
5667
5858
  if (a.project_state === 'existing' && Array.isArray(a.existing_work) && a.existing_work.length) {
5668
5859
  out.existing_work = a.existing_work.slice();
5669
5860
  if (a.main_work && a.existing_work.indexOf(a.main_work) !== -1) out.main_work = a.main_work;
5670
5861
  }
5862
+ /* the rewards only when the question was asked (never for a founder building
5863
+ alone), the suite only for the two framings that choose from it — each in
5864
+ the vocabulary's own order */
5865
+ if (Array.isArray(a.reward_intents) && !physSolo() && !physHidden('reward_intents')) {
5866
+ out.reward_intents = PHYS_REWARDS.filter(function (r) { return a.reward_intents.indexOf(r) !== -1; });
5867
+ }
5868
+ if ((a.framing === 'hybrid' || a.framing === 'local') && Array.isArray(a.services)) {
5869
+ out.services = PHYS_SERVICES.filter(function (s) { return a.services.indexOf(s) !== -1; });
5870
+ }
5871
+ /* the voice follows the entity (spec D5), stored so settings can switch it later */
5872
+ if (a.entity) out.voice = physVoice();
5671
5873
  return out;
5672
5874
  }
5875
+ function physVoice() { return PHYS_ORGS.indexOf(phys().answers.entity) !== -1 ? 'professional' : 'planet'; }
5876
+ function physSolo() { var a = phys().answers; return a.builders_at_start === 1 && a.builders_expected === 1; }
5877
+ function physHidden(option) { return physLimits.some(function (l) { return l && l.option === option && l.effect === 'hide'; }); }
5878
+ /* a question the answers skip: rewards, when the table hides them (building alone) */
5879
+ function physSkips(q) { return q === 6 && physHidden('reward_intents'); }
5880
+ /* the month-three guess the founder corrects: roughly 2.6 times the start */
5881
+ function physGuess(start) { return Math.min(100000, Math.max(start + 1, Math.round(start * 2.6))); }
5882
+ function physAnswered(q) {
5883
+ var a = phys().answers;
5884
+ if (q === 1) return !!a.project_state && (a.project_state !== 'existing' || (Array.isArray(a.existing_work) && a.existing_work.indexOf(a.main_work) !== -1));
5885
+ if (q === 2) return !!a.home_country && !!a.builder_geography;
5886
+ if (q === 3) return a.minors_on_board === true || a.minors_on_board === false;
5887
+ if (q === 4) return !!a.entity;
5888
+ if (q === 5) return Number.isInteger(a.builders_at_start) && Number.isInteger(a.builders_expected) && a.builders_expected >= a.builders_at_start;
5889
+ if (q === 6) return Array.isArray(a.reward_intents) && a.reward_intents.length > 0;
5890
+ if (q === 7) return !!a.framing;
5891
+ return false;
5892
+ }
5893
+ /* Where Continue goes from question q: the next one that applies — or, once the
5894
+ founder came here from the summary's "change", straight back to the summary
5895
+ unless a later question still needs an answer. */
5896
+ function physNextQ(q) {
5897
+ var p = phys();
5898
+ for (var n = q + 1; n <= PHYS_LAST; n++) {
5899
+ if (physSkips(n)) continue;
5900
+ if (!p.fromSum || !physAnswered(n)) return n;
5901
+ }
5902
+ return 'sum';
5903
+ }
5904
+ function physPrevQ(q) {
5905
+ var from = q === 'sum' ? PHYS_LAST + 1 : q;
5906
+ for (var n = from - 1; n >= 1; n--) if (!physSkips(n)) return n;
5907
+ return 0;
5908
+ }
5909
+
5910
+ /* ── the technical steps, pre-filled from the answers (task 1004419) ──
5911
+ What the answers suggest, applied only where the founder has not chosen for
5912
+ themselves (the field still holds its default, or what we filled last time), so
5913
+ walking back to change an answer moves the suggestion and never their choice. */
5914
+ var PHYS_PREFILL_DEFAULT = { projType: null, teamShape: null, mode: 'greenfield' };
5915
+ function physPrefillFor() {
5916
+ var a = phys().answers, out = {};
5917
+ if (PHYS_TYPE[a.entity]) out.projType = PHYS_TYPE[a.entity];
5918
+ if (physSolo()) out.teamShape = 'solo';
5919
+ if (a.project_state) out.mode = a.project_state === 'existing' && Array.isArray(a.existing_work) && a.existing_work.indexOf('code') !== -1 ? 'adopt' : 'greenfield';
5920
+ return out;
5921
+ }
5922
+ function physPrefill() {
5923
+ var want = physPrefillFor(), was = state.prefilled || {}, now = {};
5924
+ Object.keys(PHYS_PREFILL_DEFAULT).forEach(function (k) {
5925
+ var cur = state[k] === undefined ? PHYS_PREFILL_DEFAULT[k] : state[k];
5926
+ var untouched = cur === PHYS_PREFILL_DEFAULT[k] || (was[k] !== undefined && cur === was[k]);
5927
+ if (!untouched) return;
5928
+ if (want[k] !== undefined) { state[k] = want[k]; now[k] = want[k]; } else state[k] = PHYS_PREFILL_DEFAULT[k];
5929
+ });
5930
+ state.prefilled = now;
5931
+ /* a step the answers skip keeps its default answer */
5932
+ if (physStepSkipped(10)) state.appPlatform = 'later';
5933
+ if (typeof setMode === 'function') setMode(state.mode);
5934
+ if (typeof paintType === 'function') paintType(state.projType || null);
5935
+ if (typeof paintTeam === 'function') paintTeam(state.teamShape || null);
5936
+ saveDraft();
5937
+ return now;
5938
+ }
5939
+ /* The technical steps that do not apply to these answers: a planet built on its
5940
+ own machines (framing local, or open source) has no app platform to pick. A demo
5941
+ is all managed, whatever an abandoned walk through the questions left behind. */
5942
+ function physStepSkipped(n) {
5943
+ if (state.demo) return false;
5944
+ var a = (state.physics && state.physics.answers) || {};
5945
+ return n === 10 && (a.framing === 'local' || a.framing === 'open-source');
5946
+ }
5947
+ /* the wizard's Next and Back step over a skipped step; the rail still reaches it */
5948
+ function physPastSkips(n, dir) {
5949
+ while (n > 0 && n < 13 && physStepSkipped(n)) n += dir;
5950
+ return n;
5951
+ }
5952
+ /* the line on the Type and Home steps saying the answer came from the questions */
5953
+ function physPaintPrefillNotes() {
5954
+ if (!PHYS_SAY) return; /* setMode runs once at boot, above where these are set */
5955
+ var pf = state.prefilled || {}, a = (state.physics && state.physics.answers) || {};
5956
+ var whose = PHYS_ORGS.indexOf(a.entity) !== -1 ? 'your project' : 'your planet';
5957
+ var t = $('typePrefill');
5958
+ var tOn = !!pf.projType && state.projType === pf.projType && !!PHYS_SAY.entity[a.entity];
5959
+ t.hidden = !tOn;
5960
+ t.textContent = tOn ? 'Picked from your answer “' + PHYS_SAY.entity[a.entity] + '”. Change it if another fits better.' : '';
5961
+ var h = $('homePrefill');
5962
+ var hOn = !!pf.mode && state.mode === pf.mode;
5963
+ h.hidden = !hOn;
5964
+ h.textContent = !hOn ? '' : pf.mode === 'adopt'
5965
+ ? 'Adopt is picked because ' + whose + ' already has code. Choose Start fresh if it should begin again.'
5966
+ : a.project_state === 'existing'
5967
+ ? 'Start fresh is picked because what ' + whose + ' has so far is not code.'
5968
+ : 'Start fresh is picked because ' + whose + ' is new.';
5969
+ }
5673
5970
  function createDetail() {
5674
5971
  var d = physDetail();
5675
5972
  if (state.teamShape) d.team_shape = state.teamShape;
@@ -5706,13 +6003,48 @@ summary{min-height:24px;padding:3px 0;}
5706
6003
  : '/assets/world/r2-chrome-blob-one-cut.webp';
5707
6004
  var img = $('physPlanet');
5708
6005
  if (img.getAttribute('src') !== src) img.setAttribute('src', src);
5709
- img.setAttribute('data-size', String(p.q === 'stop' ? 0 : Math.max(1, Math.min(p.q, PHYS_LAST) + (a.project_state === 'existing' ? 2 : 1))));
6006
+ var size = p.q === 'stop' ? 0 : p.q === 'sum' ? 6
6007
+ : Math.min(6, Math.max(1, Math.min(typeof p.q === 'number' ? p.q : 0, PHYS_LAST) + (a.project_state === 'existing' ? 2 : 1)));
6008
+ img.setAttribute('data-size', String(size));
5710
6009
  img.hidden = p.q === 'stop';
5711
- $('physGuard').hidden = a.minors_on_board !== true;
6010
+ $('physGuard').hidden = a.minors_on_board !== true || p.q === 'stop';
5712
6011
  $('physStage').classList.toggle('physStopped', p.q === 'stop');
5713
- $('physCapN').textContent = p.q === 'stop' ? 'stopped' : (typeof p.q === 'number' ? p.q : 0) + ' of 7';
5714
- var call = 'physics · the rules it lives by · each answer shapes your planet here';
6012
+ var pro = physVoice() === 'professional';
6013
+ $('physCapLbl').textContent = pro ? 'project profile' : 'planet physics';
6014
+ $('physCapN').textContent = p.q === 'stop' ? 'stopped' : p.q === 'sum' ? 'complete' : (typeof p.q === 'number' ? p.q : 0) + ' of 7';
6015
+ /* task 1004419: moons for the builders now (beads of the core's material), faint
6016
+ ones on the dashed orbit for the expected growth, a ring per reward being
6017
+ considered, and the orbit around the platform's mark the framing sets */
6018
+ var live = p.q !== 'stop';
6019
+ var at = live && Number.isInteger(a.builders_at_start) ? a.builders_at_start : 0;
6020
+ var exp = live && Number.isInteger(a.builders_expected) ? a.builders_expected : 0;
6021
+ var bead = '/assets/world/r3-' + (mat && mat !== 'chrome' ? mat : 'frost') + '-bead-cut.webp';
6022
+ var nMoons = Math.min(at, 6), nGhosts = Math.min(Math.max(exp - at, 0), 4);
6023
+ root.querySelectorAll('#physMoons img, #physGhosts img').forEach(function (m) { if (m.getAttribute('src') !== bead) m.setAttribute('src', bead); });
6024
+ root.querySelectorAll('#physMoons img').forEach(function (m, i) { m.hidden = i >= nMoons; });
6025
+ root.querySelectorAll('#physGhosts img').forEach(function (m, i) { m.hidden = i >= nGhosts; });
6026
+ $('physMoons').hidden = nMoons === 0;
6027
+ $('physGhosts').hidden = nGhosts === 0;
6028
+ var rewards = live && Array.isArray(a.reward_intents) && !physSolo() && !physHidden('reward_intents') ? a.reward_intents : [];
6029
+ root.querySelectorAll('#physStage .physBelt').forEach(function (b, i) { b.hidden = i >= rewards.length; });
6030
+ var orbit = live && a.framing ? PHYS_ORBIT[a.framing] : 'none';
6031
+ $('physStage').setAttribute('data-orbit', orbit);
6032
+ $('physSun').hidden = orbit === 'none' || orbit === 'own';
6033
+ $('physTether').hidden = orbit === 'none' || orbit === 'own';
6034
+ var rewardWords = rewards.map(function (r) { return PHYS_SAY.reward_intents[r].toLowerCase(); }).join(', ');
6035
+ var call = pro ? 'profile · each answer shapes your project here' : 'physics · the rules it lives by · each answer shapes your planet here';
5715
6036
  if (p.q === 'stop') call = 'no planet forms · stopped at state · nothing was created';
6037
+ else if (p.q === 'sum') call = pro ? 'profile · seven answers · ready for setup' : 'physics · seven answers, one planet · ready for its coordinates';
6038
+ else if (p.q === 7 && a.framing) call = pro
6039
+ ? 'framing · ' + PHYS_SAY.framing[a.framing].toLowerCase()
6040
+ : 'orbit · ' + PHYS_SAY.framing[a.framing].toLowerCase() + ' · ' + PHYS_ORBIT_WORD[a.framing];
6041
+ else if (p.q === 6 && rewards.length) call = pro
6042
+ ? 'rewards · ' + rewards.length + ' being considered · ' + rewardWords
6043
+ : 'rings · ' + rewards.length + (rewards.length === 1 ? ' current of value · ' : ' currents of value · ') + rewardWords;
6044
+ else if (p.q === 5 && at) call = physSolo()
6045
+ ? (pro ? 'team · one person' : 'moons · just you · one moon')
6046
+ : (pro ? 'team · ' + at + (at === 1 ? ' contributor' : ' contributors') + ' · estimated ' + exp + ' by month three'
6047
+ : 'moons · ' + at + (at === 1 ? ' builder now' : ' builders now') + ' · the dashed orbit holds about ' + exp + ' by month three');
5716
6048
  else if (p.q === 4 && mat) call = 'core · ' + PHYS_CORE_WORD[mat] + ' · a ' + mat + ' core';
5717
6049
  else if (p.q === 3 && a.minors_on_board === true) call = 'protective ring · under 18s on board · the age tiers apply';
5718
6050
  else if (p.q === 2 && a.home_country) call = 'coordinates · ' + (a.home_region || a.home_country).toLowerCase() + (a.builder_geography ? ' · builders ' + PHYS_GEO_WORD[a.builder_geography] : '');
@@ -5724,24 +6056,55 @@ summary{min-height:24px;padding:3px 0;}
5724
6056
  if (a.builder_geography) coords.push('builders ' + PHYS_GEO_WORD[a.builder_geography]);
5725
6057
  if (a.minors_on_board !== undefined && a.minors_on_board !== null) coords.push(a.minors_on_board ? 'ages 13+' : 'ages 18+');
5726
6058
  if (mat) coords.push('core ' + PHYS_CORE_WORD[mat]);
6059
+ if (at) coords.push((pro ? 'team ' : 'builders ') + (physSolo() ? '1' : at + ' → ' + exp));
6060
+ if (rewards.length) coords.push('rewards ' + rewards.map(function (r) { return r === 'future-payouts' ? 'future' : r; }).join(', '));
6061
+ if (live && a.framing) coords.push((pro ? 'framing ' : 'orbit ') + a.framing);
5727
6062
  $('physCoords').textContent = coords.join(' · ');
5728
6063
  }
5729
6064
 
5730
6065
  /* The soft-limit table's verdicts (spec D6): an option it hides is hidden and
5731
6066
  one it greys is disabled, each with its reason beside it. */
6067
+ /* Task 1004419 widened it to the list questions: a member the table hides is
6068
+ hidden and one it greys disabled, and either leaves the founder's list (it can
6069
+ no longer be chosen); a member's reasons gather once each in its question's
6070
+ why list, naming the options they are about. A whole question it hides, the
6071
+ flow skips (physSkips) or hides (data-limit). */
5732
6072
  function physApplyLimits() {
6073
+ var a = phys().answers;
5733
6074
  root.querySelectorAll('#panel-physics [data-why]').forEach(function (el) { el.hidden = true; el.textContent = ''; });
5734
- root.querySelectorAll('#panel-physics [data-limit-grey]').forEach(function (el) { el.removeAttribute('data-limit-grey'); el.disabled = false; });
6075
+ root.querySelectorAll('#panel-physics [data-limit-grey]').forEach(function (el) { el.removeAttribute('data-limit-grey'); el.disabled = false; el.title = ''; });
6076
+ root.querySelectorAll('#panel-physics [data-limit-hide]').forEach(function (el) { el.removeAttribute('data-limit-hide'); el.hidden = false; });
6077
+ var lists = {};
5735
6078
  physLimits.forEach(function (l) {
5736
- var key = String(l.option || '');
6079
+ var key = String((l && l.option) || '');
6080
+ var parts = key.split(':');
5737
6081
  var why = root.querySelector('#panel-physics [data-why="' + key + '"]');
5738
- if (why && l.reason) { why.textContent = l.reason; why.hidden = false; }
5739
- if (l.effect === 'grey') {
5740
- var parts = key.split(':');
5741
- root.querySelectorAll('#panel-physics [data-phys="' + parts[0] + '"][data-val="' + (parts[1] || '') + '"]').forEach(function (b) {
5742
- b.disabled = true; b.setAttribute('data-limit-grey', '1'); b.title = l.reason || '';
5743
- });
6082
+ if (why && l.reason) { why.textContent = why.textContent ? why.textContent + ' ' + l.reason : l.reason; why.hidden = false; }
6083
+ if (!parts[1]) {
6084
+ if (l.effect === 'hide') root.querySelectorAll('#panel-physics [data-limit="' + key + '"]').forEach(function (el) { el.hidden = true; });
6085
+ return;
6086
+ }
6087
+ /* a note is offered as it is, with its reason in the list below */
6088
+ root.querySelectorAll('#panel-physics [data-phys="' + parts[0] + '"][data-val="' + parts[1] + '"]').forEach(function (b) {
6089
+ if (l.effect === 'grey') { b.disabled = true; b.setAttribute('data-limit-grey', '1'); b.title = l.reason || ''; }
6090
+ if (l.effect === 'hide') { b.hidden = true; b.setAttribute('data-limit-hide', '1'); }
6091
+ });
6092
+ if ((l.effect === 'grey' || l.effect === 'hide') && PHYS_MANY.indexOf(parts[0]) !== -1 && Array.isArray(a[parts[0]])) {
6093
+ a[parts[0]] = a[parts[0]].filter(function (v) { return v !== parts[1]; });
5744
6094
  }
6095
+ if (l.effect === 'hide' || !l.reason || !PHYS_SAY[parts[0]]) return;
6096
+ var byReason = lists[parts[0]] = lists[parts[0]] || {};
6097
+ (byReason[l.reason] = byReason[l.reason] || []).push(PHYS_SAY[parts[0]][parts[1]] || parts[1]);
6098
+ });
6099
+ root.querySelectorAll('#panel-physics [data-why-list]').forEach(function (ul) {
6100
+ while (ul.firstChild) ul.removeChild(ul.firstChild);
6101
+ var byReason = lists[ul.getAttribute('data-why-list')] || {};
6102
+ Object.keys(byReason).forEach(function (reason) {
6103
+ var li = document.createElement('li');
6104
+ li.textContent = byReason[reason].join(', ') + ': ' + reason;
6105
+ ul.appendChild(li);
6106
+ });
6107
+ ul.hidden = !Object.keys(byReason).length;
5745
6108
  });
5746
6109
  }
5747
6110
 
@@ -5753,15 +6116,25 @@ summary{min-height:24px;padding:3px 0;}
5753
6116
  root.querySelectorAll('#physRail li').forEach(function (li) {
5754
6117
  var n = Number(li.getAttribute('data-q'));
5755
6118
  li.classList.toggle('now', n === p.q);
5756
- li.classList.toggle('done', !!n && typeof p.q === 'number' && n < p.q);
6119
+ li.classList.toggle('done', !!n && (p.q === 'sum' || (typeof p.q === 'number' && n < p.q)) && !physSkips(n));
6120
+ li.classList.toggle('skipped', physSkips(n));
5757
6121
  });
5758
6122
  $('physRail').hidden = p.q === 'stop' || p.q === 0;
5759
6123
  $('physNav').hidden = p.q === 'stop';
6124
+ physPaintVoice();
5760
6125
  root.querySelectorAll('[data-phys-start]').forEach(function (b) { b.setAttribute('aria-pressed', String(b.getAttribute('data-phys-start') === p.start)); });
5761
6126
  root.querySelectorAll('#panel-physics [data-phys]').forEach(function (b) {
5762
- var v = a[b.getAttribute('data-phys')];
5763
- b.setAttribute('aria-pressed', String(v !== undefined && v !== null && String(v) === b.getAttribute('data-val')));
6127
+ var v = a[b.getAttribute('data-phys')], val = b.getAttribute('data-val');
6128
+ var on = Array.isArray(v) ? v.indexOf(val) !== -1 : v !== undefined && v !== null && String(v) === val;
6129
+ b.setAttribute('aria-pressed', String(on));
5764
6130
  });
6131
+ physPaintTeam();
6132
+ var suite = a.framing === 'hybrid' || a.framing === 'local';
6133
+ $('physSuite').hidden = !suite;
6134
+ $('physSuiteLbl').textContent = a.framing === 'local' ? 'Your local suite' : 'Your hybrid suite';
6135
+ if (p.q === 'sum') physPaintSummary();
6136
+ $('physNext').textContent = p.q !== 'sum' ? 'Continue →'
6137
+ : physVoice() === 'professional' ? 'Continue to setup →' : 'Create my planet →';
5765
6138
  var existing = a.project_state === 'existing';
5766
6139
  $('physExisting').hidden = !existing;
5767
6140
  var works = Array.isArray(a.existing_work) ? a.existing_work : [];
@@ -5781,6 +6154,97 @@ summary{min-height:24px;padding:3px 0;}
5781
6154
  physPaintStage();
5782
6155
  }
5783
6156
 
6157
+ /* the voice (spec D5): show the baked variant the entity answer picks */
6158
+ function physPaintVoice() {
6159
+ var v = physVoice() === 'professional' ? 'pro' : 'planet';
6160
+ root.querySelectorAll('#panel-physics [data-voice]').forEach(function (el) { el.hidden = el.getAttribute('data-voice') !== v; });
6161
+ }
6162
+
6163
+ /* question 5: one or many, the start count, and the month-three guess */
6164
+ function physPaintTeam() {
6165
+ var a = phys().answers, many = Number.isInteger(a.builders_at_start) && !physSolo();
6166
+ root.querySelectorAll('[data-phys-solo]').forEach(function (b) {
6167
+ var on = b.getAttribute('data-phys-solo') === 'one' ? physSolo() : many;
6168
+ b.setAttribute('aria-pressed', String(on));
6169
+ });
6170
+ $('physTeam').hidden = !many;
6171
+ if (!many) return;
6172
+ var start = a.builders_at_start, exp = a.builders_expected;
6173
+ var pro = physVoice() === 'professional';
6174
+ $('physStartN').textContent = String(start);
6175
+ $('physM1').textContent = String(start);
6176
+ $('physM2').textContent = String(Math.round(Math.sqrt(start * exp)));
6177
+ $('physM3').textContent = String(exp);
6178
+ var range = $('physExpected');
6179
+ range.setAttribute('min', String(Math.max(2, start)));
6180
+ range.setAttribute('max', String(Math.min(100000, Math.max(30, physGuess(start) * 3, exp))));
6181
+ range.value = String(exp);
6182
+ range.setAttribute('aria-valuetext', exp + (pro ? ' contributors' : ' builders') + ' by month three');
6183
+ $('physGuessLine').textContent = pro
6184
+ ? 'Based on your project, we estimate ' + exp + ' contributors by month three. Adjust it if you know better.'
6185
+ : 'From what you’re building, we’d expect about ' + exp + ' builders by month three. Nobody can estimate builder growth cold, so we guess first and you correct.';
6186
+ }
6187
+
6188
+ /* the summary: every answer, each with a way back to its question */
6189
+ function physCountryName(code) {
6190
+ try { return new Intl.DisplayNames(['en'], { type: 'region' }).of(code) || code; } catch (e) { return code; }
6191
+ }
6192
+ function physSummaryRows() {
6193
+ var a = phys().answers, pro = physVoice() === 'professional';
6194
+ var rows = [];
6195
+ var told = String(state.description || '').trim();
6196
+ rows.push({ q: 1, k: 'State', v: PHYS_SAY.project_state[a.project_state] || 'Not answered', s: told });
6197
+ rows.push({ q: 2, k: 'Geography', v: a.home_country ? physCountryName(a.home_country) + (a.home_region && PHYS_REGIONS[a.home_region] ? ' (' + PHYS_REGIONS[a.home_region] + ')' : '') : 'Not answered',
6198
+ s: PHYS_SAY.builder_geography[a.builder_geography] || '' });
6199
+ rows.push({ q: 3, k: 'Under 18', v: a.minors_on_board === true ? 'Yes' : a.minors_on_board === false ? 'No' : 'Not answered',
6200
+ s: a.minors_on_board === true ? 'The age tiers apply, and a guardian signs for each young builder' : a.minors_on_board === false ? 'Everyone building is an adult' : '' });
6201
+ rows.push({ q: 4, k: pro ? 'Entity' : 'You are', v: PHYS_SAY.entity[a.entity] || 'Not answered',
6202
+ s: PHYS_ENTITY_FORMS.indexOf(a.entity) !== -1 ? 'An entity already formed' : a.entity ? 'The binding choice comes later' : '' });
6203
+ var solo = physSolo();
6204
+ rows.push({ q: 5, k: pro ? 'Team' : 'Builders', v: solo ? 'Just you' : Number.isInteger(a.builders_at_start) ? a.builders_at_start + ' now' : 'Not answered',
6205
+ s: solo ? (pro ? 'A single contributor' : 'Building solo') : Number.isInteger(a.builders_expected) ? 'About ' + a.builders_expected + ' by month three' : '' });
6206
+ var hiddenWhy = physLimits.filter(function (l) { return l && l.option === 'reward_intents' && l.effect === 'hide'; })[0];
6207
+ var rewards = Array.isArray(a.reward_intents) ? a.reward_intents : [];
6208
+ if (physSkips(6)) rows.push({ q: 5, k: 'Rewards', v: 'Not asked', s: hiddenWhy ? hiddenWhy.reason : '' });
6209
+ else rows.push({ q: 6, k: 'Rewards', v: rewards.length ? PHYS_REWARDS.filter(function (r) { return rewards.indexOf(r) !== -1; }).map(function (r) { return PHYS_SAY.reward_intents[r]; }).join(', ') : 'Not answered',
6210
+ s: rewards.length ? 'Intent only' : '' });
6211
+ var services = Array.isArray(a.services) ? a.services : [];
6212
+ rows.push({ q: 7, k: 'Framing', v: PHYS_SAY.framing[a.framing] || 'Not answered',
6213
+ s: a.framing === 'managed' ? 'The whole suite, run for you'
6214
+ : a.framing === 'open-source' ? 'The free core, on your own machines'
6215
+ : (a.framing === 'hybrid' || a.framing === 'local') ? (services.length ? PHYS_SERVICES.filter(function (x) { return services.indexOf(x) !== -1; }).map(function (x) { return PHYS_SAY.services[x]; }).join(', ') : 'No services picked yet') : '' });
6216
+ return rows;
6217
+ }
6218
+ function physPaintSummary() {
6219
+ var dl = $('physSummary');
6220
+ while (dl.firstChild) dl.removeChild(dl.firstChild);
6221
+ physSummaryRows().forEach(function (r) {
6222
+ var dt = document.createElement('dt');
6223
+ dt.textContent = r.k;
6224
+ var dd = document.createElement('dd');
6225
+ var body = document.createElement('span');
6226
+ body.textContent = r.v;
6227
+ if (r.s) { var sm = document.createElement('small'); sm.textContent = r.s; body.appendChild(sm); }
6228
+ var change = document.createElement('button');
6229
+ change.type = 'button';
6230
+ change.className = 'physLink';
6231
+ change.textContent = 'change';
6232
+ change.setAttribute('data-phys-goto', String(r.q));
6233
+ change.setAttribute('aria-label', 'Change ' + r.k.toLowerCase());
6234
+ change.addEventListener('click', function () { physChange(r.q); });
6235
+ dd.appendChild(body);
6236
+ dd.appendChild(change);
6237
+ dl.appendChild(dt);
6238
+ dl.appendChild(dd);
6239
+ });
6240
+ }
6241
+ /* "change": back to that question; Continue from it returns to the summary */
6242
+ function physChange(q) {
6243
+ phys().fromSum = true;
6244
+ physHint('');
6245
+ physGo(q);
6246
+ }
6247
+
5784
6248
  function physHint(msg) {
5785
6249
  $('h-phys').textContent = msg || ' ';
5786
6250
  $('h-phys').className = msg ? 'hint err' : 'hint';
@@ -5793,6 +6257,7 @@ summary{min-height:24px;padding:3px 0;}
5793
6257
  }
5794
6258
  function physGo(q) {
5795
6259
  phys().q = q;
6260
+ if (q === 'sum') phys().fromSum = false;
5796
6261
  saveDraft();
5797
6262
  paintPhysics();
5798
6263
  var h = root.querySelector('#panel-physics .physQ[data-q="' + q + '"] h1');
@@ -5819,22 +6284,43 @@ summary{min-height:24px;padding:3px 0;}
5819
6284
  physBusy = false;
5820
6285
  if (res && res.stop) { $('physStopMsg').textContent = res.message || ''; physGo('stop'); return; }
5821
6286
  physLimits = (res && res.soft_limits && res.soft_limits.limits) || [];
5822
- physGo(2);
6287
+ physGo(physNextQ(1));
5823
6288
  });
5824
6289
  return false;
5825
6290
  }
5826
6291
  if (p.q === 2) {
5827
6292
  if (!a.home_country) { physHint('Pick your country.'); return false; }
5828
6293
  if (!a.builder_geography) { physHint('Say where you expect builders to be. Not sure yet is a fine answer.'); return false; }
5829
- physGo(3); return false;
6294
+ physGo(physNextQ(2)); return false;
5830
6295
  }
5831
6296
  if (p.q === 3) {
5832
6297
  if (a.minors_on_board !== true && a.minors_on_board !== false) { physHint('Answer yes or no to go on.'); return false; }
5833
- physGo(4); return false;
6298
+ physGo(physNextQ(3)); return false;
6299
+ }
6300
+ /* from question 4 on, the table is asked again on every Continue: what it hides
6301
+ or greys on the next screens depends on the answers just given */
6302
+ if (p.q >= 4 && p.q <= PHYS_LAST) {
6303
+ if (!physAnswered(p.q)) {
6304
+ physHint(p.q === 4 ? 'Pick the one that fits best. Don’t know yet is a fine answer.'
6305
+ : p.q === 5 ? 'Say one or many to go on.'
6306
+ : p.q === 6 ? 'Pick at least one. Non-monetary is a fine answer.'
6307
+ : 'Pick how much should be run for you to go on.');
6308
+ return false;
6309
+ }
6310
+ if (physBusy) return false;
6311
+ physBusy = true;
6312
+ var from = p.q;
6313
+ physCheck().then(function (res) {
6314
+ physBusy = false;
6315
+ if (res && res.soft_limits) physLimits = res.soft_limits.limits || [];
6316
+ physGo(physNextQ(from));
6317
+ });
6318
+ return false;
5834
6319
  }
5835
- if (p.q === 4) {
5836
- if (!a.entity) { physHint('Pick the one that fits best. Don’t know yet is a fine answer.'); return false; }
5837
- physCheck().then(function (res) { physLimits = (res && res.soft_limits && res.soft_limits.limits) || []; });
6320
+ /* the summary's Create my planet: the answers pre-fill the technical steps,
6321
+ and the wizard moves on to the first of them, the name */
6322
+ if (p.q === 'sum') {
6323
+ physPrefill();
5838
6324
  show(4, true);
5839
6325
  return false;
5840
6326
  }
@@ -5844,7 +6330,8 @@ summary{min-height:24px;padding:3px 0;}
5844
6330
  $('physBack').addEventListener('click', function () {
5845
6331
  var p = phys();
5846
6332
  physHint('');
5847
- if (typeof p.q === 'number' && p.q > 0) physGo(p.q - 1); else show(2, true);
6333
+ p.fromSum = false;
6334
+ if (p.q === 'sum' || (typeof p.q === 'number' && p.q > 0)) physGo(physPrevQ(p.q)); else show(2, true);
5848
6335
  });
5849
6336
  $('physStartOver').addEventListener('click', function () {
5850
6337
  state.description = '';
@@ -5866,12 +6353,64 @@ summary{min-height:24px;padding:3px 0;}
5866
6353
  b.addEventListener('click', function () {
5867
6354
  var k = b.getAttribute('data-phys'), raw = b.getAttribute('data-val');
5868
6355
  var v = raw === 'true' ? true : raw === 'false' ? false : raw;
5869
- var a = phys().answers;
5870
- a[k] = a[k] === v ? null : v; /* a second click un-says it, like the team pills */
6356
+ var p = phys(), a = p.answers;
6357
+ if (PHYS_MANY.indexOf(k) !== -1) {
6358
+ /* a list question: each click adds or takes away one member */
6359
+ var list = Array.isArray(a[k]) ? a[k].slice() : [];
6360
+ var at = list.indexOf(v);
6361
+ if (at === -1) list.push(v); else list.splice(at, 1);
6362
+ a[k] = list;
6363
+ if (k === 'services') p.suiteTouched = true;
6364
+ } else {
6365
+ a[k] = a[k] === v ? null : v; /* a second click un-says it, like the team pills */
6366
+ }
5871
6367
  if (k === 'project_state' && a[k] !== 'existing') { delete a.existing_work; delete a.main_work; }
6368
+ if (k === 'framing') physFramingChanged();
5872
6369
  physHint(''); saveDraft(); paintPhysics();
5873
6370
  });
5874
6371
  });
6372
+ /* A hybrid or local framing starts from the canvas's suite until the founder picks
6373
+ for themselves; the table is asked again, since what it hides follows the framing. */
6374
+ function physFramingChanged() {
6375
+ var p = phys(), a = p.answers;
6376
+ if ((a.framing === 'hybrid' || a.framing === 'local') && !p.suiteTouched) a.services = PHYS_SUITE_START[a.framing].slice();
6377
+ physCheck().then(function (res) {
6378
+ if (!res || !res.soft_limits) return;
6379
+ physLimits = res.soft_limits.limits || [];
6380
+ paintPhysics();
6381
+ });
6382
+ }
6383
+ /* question 5's controls: one or many, the start count, the guess */
6384
+ root.querySelectorAll('[data-phys-solo]').forEach(function (b) {
6385
+ b.addEventListener('click', function () {
6386
+ var p = phys(), a = p.answers;
6387
+ if (b.getAttribute('data-phys-solo') === 'one') {
6388
+ a.builders_at_start = 1; a.builders_expected = 1; p.guessTouched = false;
6389
+ } else if (physSolo() || !Number.isInteger(a.builders_at_start)) {
6390
+ a.builders_at_start = 2; a.builders_expected = physGuess(2); p.guessTouched = false;
6391
+ }
6392
+ physHint(''); saveDraft(); paintPhysics();
6393
+ });
6394
+ });
6395
+ root.querySelectorAll('[data-phys-count]').forEach(function (b) {
6396
+ b.addEventListener('click', function () {
6397
+ var p = phys(), a = p.answers;
6398
+ var start = Math.min(100000, Math.max(1, (Number.isInteger(a.builders_at_start) ? a.builders_at_start : 2) + Number(b.getAttribute('data-phys-count'))));
6399
+ a.builders_at_start = start;
6400
+ /* the guess follows the start until the founder corrects it; "many" always
6401
+ expects company, or it would read as building alone */
6402
+ if (!p.guessTouched) a.builders_expected = physGuess(start);
6403
+ a.builders_expected = Math.max(a.builders_expected || 0, start, 2);
6404
+ saveDraft(); paintPhysics();
6405
+ });
6406
+ });
6407
+ $('physExpected').addEventListener('input', function () {
6408
+ var p = phys(), a = p.answers, n = Math.round(Number($('physExpected').value));
6409
+ if (!Number.isFinite(n)) return;
6410
+ a.builders_expected = Math.max(n, a.builders_at_start || 1, 2);
6411
+ p.guessTouched = true;
6412
+ saveDraft(); physPaintTeam(); physPaintStage();
6413
+ });
5875
6414
  root.querySelectorAll('#physExisting [data-work]').forEach(function (cb) {
5876
6415
  cb.addEventListener('change', function () {
5877
6416
  var a = phys().answers, w = cb.getAttribute('data-work');
@@ -6029,11 +6568,12 @@ summary{min-height:24px;padding:3px 0;}
6029
6568
  form.addEventListener('submit', function (ev) {
6030
6569
  ev.preventDefault();
6031
6570
  var next = Number(form.getAttribute('data-next'));
6032
- if (validateStep(next - 1)) show(next, true);
6571
+ /* a step the planet-physics answers skip is stepped over (task 1004419) */
6572
+ if (validateStep(next - 1)) show(physPastSkips(next, 1), true);
6033
6573
  });
6034
6574
  });
6035
6575
  root.querySelectorAll('[data-back]').forEach(function (btn) {
6036
- btn.addEventListener('click', function () { show(Number(btn.getAttribute('data-back')), true); });
6576
+ btn.addEventListener('click', function () { show(physPastSkips(Number(btn.getAttribute('data-back')), -1), true); });
6037
6577
  });
6038
6578
 
6039
6579
  /* ── review ── */
@@ -6053,6 +6593,8 @@ summary{min-height:24px;padding:3px 0;}
6053
6593
  root.querySelectorAll('#appOpts input[name=appPlatform]').forEach(function (i) { i.checked = i.value === pick; });
6054
6594
  }
6055
6595
  function appRow() {
6596
+ /* the step the framing skipped (task 1004419) */
6597
+ if (physStepSkipped(10)) return 'Runs on your own machines, as your framing says — you can choose a platform later from the project’s page';
6056
6598
  if (appPlatformChoice() !== 'render') return 'Not chosen yet — you can decide later';
6057
6599
  if (renderLater()) return 'Render — set up from your project’s page once your app has code. Nothing is created on Render now.';
6058
6600
  var ws =esc(state.renderOwnerName || state.renderOwnerId || '');