studio-engine 0.74.7 → 0.74.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,9 +14,9 @@
14
14
  card behind ours. So this section owns a tiny, self-contained store + overlay
15
15
  and registers all specimen content itself.
16
16
 
17
- Groups: Auth (the sign-in step machine), Eligibility and entry (the
18
- app-supplied-policy age-gate DOB primitive, gated by Studio.feature?(:age_gate),
19
- plus the app-specific entry-tokens purchase flow documented via engine chrome),
17
+ Groups: Auth (the engine-owned FRAGMENTS an app's own sign-in card is built
18
+ from the engine ships no auth card), Eligibility and entry (the
19
+ app-supplied-policy age-gate DOB primitive, gated by Studio.feature?(:age_gate)),
20
20
  Profile (crop / upload, opened
21
21
  through studio/cropper_assets), Web3 (wallet-connect and web3-step-up now
22
22
  rendered from the solana-studio gem / on-chain-tx / deposit / the generic
@@ -86,70 +86,51 @@
86
86
  # alone is false, so the card renders as a NORMAL live card: no badge, no
87
87
  # aria-disabled, and clickable whatever `openable` says. That is the worse
88
88
  # of the two, and it is the exact scenario the BASE-app test runs in.
89
- # The other six web3 specimens are engine-owned and registered
90
- # unconditionally, so they keep the plain capability gate.
89
+ # The ONE other web3 specimen (Entry confirmed) is engine-owned and registered
90
+ # unconditionally, so it keeps the plain capability gate. That population was
91
+ # SIX until 2026-09-09: on-chain-tx (x3), ds-wallet-topup and ds-onramp-hub
92
+ # were mirrors of turf-monster cards and were retired with the rest of them.
93
+ # Recorded here because a gate guarding one card reads like an oversight, and
94
+ # the next reader deserves to know it guarded six and still applies.
91
95
  web3_card_off = !web3_on || !web3_gem
92
96
 
93
- # Auth method-toggle defaults mirror the app's Studio.auth_method? config;
94
- # Solana Wallet also needs the web3 capability, so it defaults OFF on an app
95
- # (like McRitchie Studio) that ships web3 off.
97
+ # THE AUTH METHOD-TOGGLE DEFAULTS USED TO LIVE HERE (ml_default / g_default /
98
+ # w_default / auth_toggles) and are gone with the card they configured. They
99
+ # belonged to style/modals/_auth, a MIRROR of turf-monster's sign-in modal,
100
+ # retired on 2026-09-09. The engine ships no auth card to configure.
96
101
  #
97
- # web3_gem is the THIRD term, and it answers a question neither of the other
98
- # two asks. auth_methods declares which CREDENTIALS an app accepts; features
99
- # gates PRODUCT SURFACES lib/studio.rb spells that split out where it
100
- # deliberately does NOT gate the /auth/solana routes on :web3. Both are
101
- # POLICY. Whether the wallet picker can RENDER AT ALL is template resolution,
102
- # and the Solana button does nothing except swap to that picker. A trigger
103
- # must carry at least the gate its target's REGISTRATION carries (see
104
- # web3_gem above, which wraps the wallet-connect registration) or it opens an
105
- # empty panel — the same failure the three specimen cards had, through a
106
- # different door.
107
- ml_default = Studio.auth_method?(:magic_link)
108
- g_default = Studio.auth_method?(:google)
109
- w_default = Studio.auth_method?(:wallet) && web3_on && web3_gem
110
-
111
- # The Sign in card's toggles. Solana Wallet is DROPPED where the gem is
112
- # absent, because that checkbox is the door this page actually opens:
113
- # methodOn('wallet') reads props.methods.wallet — the toggle's own boolean —
114
- # BEFORE it ever falls back to the defaults above, so a ticked box overrides
115
- # w_default outright. Leaving it offered would keep the empty panel one click
116
- # away in EVERY base-app configuration, including the stock auth_methods
117
- # default that declares no :wallet at all. A control that cannot do its one
118
- # job is worse than an absent one; the section prose says why it is gone.
119
- auth_toggles = [
120
- { model: "opts.magicLink", label: "Magic Link" },
121
- { model: "opts.google", label: "Google" }
122
- ]
123
- auth_toggles << { model: "opts.wallet", label: "Solana Wallet" } if web3_gem
124
- auth_toggles << { model: "opts.terms", label: "Terms" }
102
+ # The rule they were built to enforce OUTLIVES them and is worth keeping: a
103
+ # TRIGGER must carry at least the gate its target's REGISTRATION carries, or
104
+ # it opens an empty panel. auth_methods declares which CREDENTIALS an app
105
+ # accepts and features gates PRODUCT SURFACES both POLICY while whether a
106
+ # card can RENDER AT ALL is template resolution, which is what web3_gem above
107
+ # answers. The three gem-backed web3 specimens are where that rule is still
108
+ # load-bearing on this page.
125
109
 
126
110
  # Active-card glow-match expression builder: yields an Alpine boolean that is
127
111
  # true when $store.dsModals.current() is THIS specimen's modal. It discriminates
128
112
  # by sub-state so specimens that open the SAME modal id still glow the right
129
113
  # card, and so the glow FOLLOWS a walked flow as the step machine advances:
130
- # step: the auth / entry-tokens step machine (step_default is the
131
- # modal's own default step — 'credentials' for auth, 'picker'
132
- # for entry-tokens).
133
- # state: the on-chain-tx state (processing / success / error) — lets the
134
- # glow track Connect Wallet -> Processing -> On-chain success.
135
114
  # crop: the two crop-photo variants (:picker vs :crop), on props.cropReady.
136
115
  # celebrate: the input specimen (form) vs the "updated" (celebrate) specimen
137
116
  # of the SAME leveling modal id.
138
117
  # provider: PRESENCE, not a value — the two web3-step-up specimens differ
139
118
  # only by whether a wallet BRAND is remembered, and the brand is
140
119
  # a free-form string (phantom / solflare / backpack, or nil), so
141
- # there is no fixed value to match on the way step: and state:
142
- # match. true = the remembered-brand card, false = the picker
120
+ # there is no fixed value to match on the way crop: and
121
+ # celebrate: match. true = the remembered-brand card, false = the picker
143
122
  # fallback. Without this the pair shared one id with no
144
123
  # discriminator, only the first card carried a glow_when, and
145
124
  # opening the SECOND card lit the FIRST — the one bug a glow
146
125
  # helper exists to prevent.
147
- ds_glow = lambda do |id, step: nil, step_default: "credentials", crop: nil, celebrate: nil, state: nil,
148
- provider: nil|
126
+ # step:/state: were dropped on 2026-09-09 with the walked flows that used them
127
+ # (auth's credentials -> magic-link-sent machine, and on-chain-tx's
128
+ # processing/success/error). Both were mirrors of turf-monster cards. A
129
+ # discriminator with no caller cannot be shown to work, so it goes rather than
130
+ # waits: the pattern is three lines and is in this file's history.
131
+ ds_glow = lambda do |id, crop: nil, celebrate: nil, provider: nil|
149
132
  cur = "$store.dsModals.current()"
150
133
  expr = "#{cur} && #{cur}.id === '#{id}'"
151
- expr += " && (#{cur}.props.step || '#{step_default}') === '#{step}'" if step
152
- expr += " && #{cur}.props.state === '#{state}'" if state
153
134
  expr += (crop == :crop ? " && !!#{cur}.props.cropReady" : " && !#{cur}.props.cropReady") unless crop.nil?
154
135
  expr += (celebrate ? " && !!#{cur}.props.celebrate" : " && !#{cur}.props.celebrate") unless celebrate.nil?
155
136
  expr += (provider ? " && !!#{cur}.props.provider" : " && !#{cur}.props.provider") unless provider.nil?
@@ -158,15 +139,20 @@
158
139
  %>
159
140
  <%# DEMO flow wiring (style-page only): the age-gate primitive fires an
160
141
  'age-verified' window event on a successful confirm — its designed handoff
161
- hook, the same one a production app listens for to resume the entry. Here the
162
- style page plays that "app": on age-verified it opens the entry-token purchase
163
- modal, so the DS walks eligibility to purchase in sequence. The engine
164
- primitive is untouched; its real submit still posts to the app-supplied
165
- submit_url. Kept on the section (always mounted) so it fires even as the
166
- age-gate card closes. %>
142
+ hook, the same one a production app listens for to resume the entry it was
143
+ gating. Here the style page plays that "app" and resumes to Entry confirmed,
144
+ so the walk from eligibility to a finished entry runs in sequence.
145
+ WHAT IS BEING DEMONSTRATED IS THE EVENT, NOT THE DESTINATION. Which card an
146
+ app resumes to is the app's business this page has to pick something, and
147
+ it picks an ENGINE-OWNED card so the demo cannot drift from a consumer's. It
148
+ resumed to the entry-token purchase flow until 2026-09-09; that flow was a
149
+ mirror of turf-monster's and was retired, which is the only reason this
150
+ target moved. The engine primitive is untouched; its real submit still posts
151
+ to the app-supplied submit_url. Kept on the section (always mounted) so it
152
+ fires even as the age-gate card closes. %>
167
153
  <section id="modals" class="space-y-10" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)"
168
154
  x-data="{}" @ds-modal-close.window="$store.dsModals.close()"
169
- @age-verified.window="$store.dsModals.open('entry-tokens', { step: 'picker' })">
155
+ @age-verified.window="$store.dsModals.open('entry-confirmed', { txSignature: null, lobbyUrl: null, seedsEarned: 0 })">
170
156
  <div class="space-y-1">
171
157
  <h2 class="text-2xl font-bold text-heading">Modals</h2>
172
158
  <p class="text-muted text-sm">
@@ -176,7 +162,7 @@
176
162
  modal (Enter/Space work too). The header <strong>Copy</strong> yields an
177
163
  agent-ready reference you can paste into a chat; option <strong>toggles</strong>
178
164
  configure the modal before you open it; and the card that matches the open
179
- modal + step <strong>glows</strong>, following the step machine as it advances.
165
+ modal <strong>glows</strong>.
180
166
  </p>
181
167
  </div>
182
168
 
@@ -184,21 +170,17 @@
184
170
  The full store API — open(id, props, opts) LIFO stack, swap() directional
185
171
  slide, advance(patch, opts) in-modal step change, close / closeAll,
186
172
  current(), cardClasses() driving the enter/exit animation registry, and
187
- body.modal-open scroll lock. Plus the dsSolanaModal proxy over the store
188
- (stable show/success/error/close API for the on-chain-tx modal) and the
189
- credential stubs (postMagicLink / walletProvider / solanaWalletConnect) so
190
- the auth + wallet specimens run without an auth backend. Defined in a plain
173
+ body.modal-open scroll lock. Plus the walletProvider stub required by
174
+ solana-studio's picker so the wallet specimens run without a real
175
+ extension. Defined in a plain
191
176
  inline script (no importmap dependency) so Alpine's defer finds it. %>
192
177
  <script>
193
178
  (function () {
194
179
  // === Credential stubs — never clobber a real one an app already ships. ==
195
- window.postMagicLink = window.postMagicLink || function () {
196
- return Promise.resolve({ success: true });
197
- };
198
- window.solanaWalletConnect = window.solanaWalletConnect || function () {
199
- return { connecting: false, error: null, statusText: 'Connect Wallet',
200
- walletAvailable: false, isMobile: false, connect: function () {} };
201
- };
180
+ // postMagicLink and solanaWalletConnect were retired here on 2026-09-09
181
+ // with style/modals/_auth, the mirror of turf-monster's sign-in card that
182
+ // was their only caller. What remains is what solana-studio's own picker
183
+ // requires, which is why walletProvider stays and they did not.
202
184
  // Connect stub for the wallet picker (solana-studio
203
185
  // solana_studio/modals/_wallet_connect, configured by
204
186
  // style/modals/_wallet_connect). Performs NO connect: it
@@ -215,7 +197,7 @@
215
197
  isAvailable: function () { return false; }
216
198
  };
217
199
 
218
- // Register the page-scoped dsModals + dsSolanaModal stores. Named (not an
200
+ // Register the page-scoped dsModals store. Named (not an
219
201
  // inline alpine:init callback) so the dual-guard at the end can invoke it
220
202
  // directly on a Turbo visit, when alpine:init will not fire again.
221
203
  function registerDsModals() {
@@ -355,23 +337,14 @@
355
337
  }
356
338
  });
357
339
 
358
- // dsSolanaModal stateless proxy over dsModals for the on-chain-tx
359
- // modal. Reads the current 'onchain-tx' entry's props so the partial's
360
- // $store.dsSolanaModal.{state,title,txSignature,...} reads stay stable.
361
- Alpine.store('dsSolanaModal', {
362
- _cur: function () { var c = Alpine.store('dsModals').current(); return (c && c.id === 'onchain-tx') ? c : null; },
363
- _read: function (k, fb) { var c = this._cur(); return c ? (c.props[k] !== undefined ? c.props[k] : fb) : fb; },
364
- close: function () { if (this._cur()) Alpine.store('dsModals').close(); },
365
- get state() { return this._read('state', null); },
366
- get title() { return this._read('title', ''); },
367
- get message() { return this._read('message', ''); },
368
- get txSignature() { return this._read('txSignature', null); },
369
- get errorMessage() { return this._read('errorMessage', ''); },
370
- get successTitle() { return this._read('successTitle', ''); },
371
- get successSubtitle() { return this._read('successSubtitle', ''); },
372
- get ctaLabel() { return this._read('ctaLabel', ''); },
373
- get ctaHref() { return this._read('ctaHref', ''); }
374
- });
340
+ // A SECOND STORE USED TO BE REGISTERED HERE: dsSolanaModal, a stateless
341
+ // proxy that re-read the current 'onchain-tx' entry's props so
342
+ // style/modals/_onchain_tx could keep its
343
+ // $store.dsSolanaModal.{state,title,txSignature,…} reads. That partial
344
+ // was a mirror of turf-monster's card and was retired on 2026-09-09,
345
+ // leaving the proxy with no reader, so it went with it. A store with no
346
+ // reader still registers, still looks load-bearing, and still has to be
347
+ // reasoned about by everyone who reads this file.
375
348
  }
376
349
 
377
350
  // Alpine's deferred CDN <script> fires alpine:init exactly ONCE, on the
@@ -425,13 +398,17 @@
425
398
  <div class="bg-surface rounded-xl border border-subtle shadow-2xl p-6 max-w-sm w-full"
426
399
  :class="$store.dsModals.cardClasses()">
427
400
 
428
- <%# --- Auth suite --- %>
429
- <%# web3_gem is passed, not recomputed: this file owns the lookup (see
430
- above) and the auth modal's Solana button needs the SAME answer,
431
- because that button's only job is to swap to the wallet-connect
432
- registration gated on it a few lines down. One lookup, two readers. %>
433
- <template x-if="$store.dsModals.current().id === 'auth'">
434
- <div><%= render "style/modals/auth", web3_gem: web3_gem %></div>
401
+ <%# --- Auth fragments --- %>
402
+ <%# NOT an auth card. The engine owns no sign-in modal — each app cards
403
+ its own on its own host section (turf-monster: style/host/_modals).
404
+ These two register the PIECES every app's auth card is built from,
405
+ which had no specimen until style/modals/_auth, a mirror of turf's
406
+ card, was retired on 2026-09-09 and took their only render with it. %>
407
+ <template x-if="$store.dsModals.current().id === 'ds-email-field'">
408
+ <div><%= render "style/modals/ds_email_field" %></div>
409
+ </template>
410
+ <template x-if="$store.dsModals.current().id === 'ds-resend-footer'">
411
+ <div><%= render "style/modals/ds_resend_footer" %></div>
435
412
  </template>
436
413
 
437
414
  <%# --- Onboarding step (its card now lives in Profile) --- %>
@@ -453,9 +430,6 @@
453
430
  <template x-if="$store.dsModals.current().id === 'age-gate'">
454
431
  <div><%= render "style/modals/age_gate" %></div>
455
432
  </template>
456
- <template x-if="$store.dsModals.current().id === 'entry-tokens'">
457
- <div><%= render "style/modals/entry_tokens" %></div>
458
- </template>
459
433
 
460
434
  <%# --- Profile (real engine partials, opened on dsModals) --- %>
461
435
  <template x-if="$store.dsModals.current().id === 'crop-photo'">
@@ -478,6 +452,10 @@
478
452
  <template x-if="$store.dsModals.current().id === 'email-change-pending'">
479
453
  <div><%= render "style/modals/email_change_pending" %></div>
480
454
  </template>
455
+ <%# --- the stack-mechanics vehicle (see Stack behaviour, bottom of page) --- %>
456
+ <template x-if="$store.dsModals.current().id === 'ds-stack-demo'">
457
+ <div><%= render "style/modals/ds_stack_demo" %></div>
458
+ </template>
481
459
  <%# --- funding chrome primitives (extract-funding-modal-chrome) --- %>
482
460
  <template x-if="$store.dsModals.current().id === 'ds-rail-row'">
483
461
  <div><%= render "style/modals/ds_rail_row" %></div>
@@ -485,13 +463,6 @@
485
463
  <template x-if="$store.dsModals.current().id === 'ds-close-x'">
486
464
  <div><%= render "style/modals/ds_close_x" %></div>
487
465
  </template>
488
- <%# --- the funding modals those primitives were extracted for --- %>
489
- <template x-if="$store.dsModals.current().id === 'ds-wallet-topup'">
490
- <div><%= render "style/modals/ds_wallet_topup" %></div>
491
- </template>
492
- <template x-if="$store.dsModals.current().id === 'ds-onramp-hub'">
493
- <div><%= render "style/modals/ds_onramp_hub" %></div>
494
- </template>
495
466
  <template x-if="$store.dsModals.current().id === 'rate-limit-general'">
496
467
  <div><%= render "style/modals/rate_limit_general" %></div>
497
468
  </template>
@@ -517,9 +488,6 @@
517
488
  <div><%= render "solana_studio/modals/web3_step_up", modal_store: "dsModals" %></div>
518
489
  </template>
519
490
  <% end %>
520
- <template x-if="$store.dsModals.current().id === 'onchain-tx'">
521
- <div><%= render "style/modals/onchain_tx" %></div>
522
- </template>
523
491
  <%# entry-confirmed — the engine celebration composed via _success_card's
524
492
  yield. One registration serves the generic (web3) + seeds (leveling)
525
493
  specimens; the seeds bar self-gates on Studio.feature?(:leveling). %>
@@ -650,72 +618,50 @@
650
618
  <div class="space-y-1">
651
619
  <h3 class="text-xl font-bold text-heading">Auth</h3>
652
620
  <p class="text-muted text-sm">
653
- The sign-in step machine
654
- (<code class="font-mono text-2xs">style/modals/_auth</code>) one modal,
655
- internal steps <code class="font-mono text-2xs">credentials &rarr;
656
- magic-link-sent &rarr; magic-link-resent</code>. Toggle the methods below a
657
- card, then open it to watch them gate the modal. The live card
658
- <strong>glows</strong>, and the glow follows the step machine as you advance.
659
- <% unless web3_gem %>
660
- <strong>Solana sign-in needs solana-studio, which this app does not
661
- bundle</strong>, so that button and its toggle are absent here &mdash;
662
- nothing registers the wallet picker the button opens.
663
- <% end %>
621
+ <strong>The engine ships no sign-in card.</strong> A sign-in modal is a
622
+ product decision &mdash; which methods, what terms, whose legal copy
623
+ &mdash; so each app owns its own and cards it on <em>its</em> section of
624
+ this guide, against the partial that actually renders in production.
625
+ What the engine owns is the <strong>pieces</strong> that card is built
626
+ from, and they are below.
627
+ </p>
628
+ <p class="text-muted text-sm">
629
+ Until 2026-09-09 this section held a full auth modal
630
+ (<code class="font-mono text-2xs">style/modals/_auth</code>). It was a
631
+ <strong>mirror</strong> of turf-monster&rsquo;s card, and it drifted:
632
+ reviewing it told you about a copy nobody shipped. It is gone, and these
633
+ two fragments &mdash; which it happened to be the only renderer of &mdash;
634
+ now stand on their own.
664
635
  </p>
665
636
  </div>
666
637
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
667
- <%# Sign inmethod toggles + the credentials-step glow. %>
638
+ <%# email fieldthe one piece every app's auth card renders. %>
668
639
  <%= render layout: "style/modal_specimen", locals: {
669
- label: "Sign in",
670
- reference: %(the Auth "Sign in" modal (studio-engine style/modals/_auth, step 'credentials') — open with $store.dsModals.open('auth', { step: 'credentials', picksRequired: 6 }); toggle methods via props.methods { magicLink, google, wallet } + props.terms),
671
- card_data: "opts: { magicLink: #{ml_default}, google: #{g_default}, wallet: #{w_default}, terms: true }",
672
- open_expr: "$store.dsModals.open('auth', { step: 'credentials', picksRequired: 6, methods: { magicLink: opts.magicLink, google: opts.google, wallet: opts.wallet }, terms: opts.terms })",
673
- glow_when: ds_glow.call("auth", step: "credentials"),
674
- toggles: auth_toggles } do %>
675
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
676
- <span class="block h-2 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
677
- <span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
640
+ label: "Email field",
641
+ reference: %(the modal family's email input (studio/modals/shared/_email_field) — a themed <input type="email"> that either posts through its own `name` or feeds an enclosing x-model, with locals name/id/value/placeholder/required/autocomplete/autofocus/input_class/x_model/x_ref/disabled_expr/validator. THE `validator` LOCAL IS A FORK, NOT A FLAG: off, the partial is self-contained and the SERVER stays the only boundary; on, it wraps itself in x-data="emailValidator()" and paints a right-edge spinner/check/X that only works if the CONSUMER ships that factory, so passing it without one yields an Alpine scope error and a dead indicator. The specimen draws both faces side by side; this guide ships no emailValidator, which is why the second indicator stays blank. Open with $store.dsModals.open('ds-email-field')),
642
+ open_expr: "$store.dsModals.open('ds-email-field')",
643
+ glow_when: ds_glow.call("ds-email-field") } do %>
644
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
645
+ <span class="block h-1.5 w-12 rounded" style="background: var(--color-text); opacity: .18"></span>
678
646
  <span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
679
- <span class="block h-6 w-full rounded" style="background: var(--color-cta); opacity: .5"></span>
647
+ <span class="flex items-center h-6 w-full rounded border px-1.5" style="border-color: var(--color-border-strong)">
648
+ <span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .12"></span>
649
+ <span class="block w-3 h-3 rounded-full ml-1" style="background: var(--color-success); opacity: .55"></span>
650
+ </span>
680
651
  </div>
681
652
  <% end %>
682
653
 
654
+ <%# resend footer — the piece both link-sent steps share. %>
683
655
  <%= render layout: "style/modal_specimen", locals: {
684
- label: "Magic link sent",
685
- reference: %(the Auth "Magic link sent" step (style/modals/_auth, step 'magic-link-sent') open with $store.dsModals.open('auth', { step: 'magic-link-sent', sentEmail: 'you@example.com' })),
686
- open_expr: "$store.dsModals.open('auth', { step: 'magic-link-sent', sentEmail: 'you@example.com' })",
687
- glow_when: ds_glow.call("auth", step: "magic-link-sent") } do %>
656
+ label: "Resend footer",
657
+ reference: %(the magic-link resend footer (studio/modals/auth/_resend_footer) — the resend link, its error line and the Close beneath it, shared by an auth card's magic-link-sent and magic-link-resent steps because the two are identical, which is why it was extracted. IT RESOLVES FROM AN ENCLOSING x-data, NOT A STORE: it reads props.submitting, props.resendCooldown and props.resendError and calls resendMagicLink(), so a scope supplying none of them fails SILENTLY — nothing errors, the link simply never enables and the cooldown never counts. The specimen supplies a stand-in host so all three states are reachable, and shortens the cooldown from production's 60s to 5s because the partial does not own the duration, the host does. Open with $store.dsModals.open('ds-resend-footer')),
658
+ open_expr: "$store.dsModals.open('ds-resend-footer')",
659
+ glow_when: ds_glow.call("ds-resend-footer") } do %>
688
660
  <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
689
661
  <div class="text-2xl leading-none">📬</div>
690
- <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
691
- <span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
692
- </div>
693
- <% end %>
694
-
695
- <%# NEW — the magic-link-resent step (the resend confirmation slide). %>
696
- <%= render layout: "style/modal_specimen", locals: {
697
- label: "Magic link resent",
698
- reference: %(the Auth "Magic link resent" step (style/modals/_auth, step 'magic-link-resent') — open with $store.dsModals.open('auth', { step: 'magic-link-resent', sentEmail: 'you@example.com' })),
699
- open_expr: "$store.dsModals.open('auth', { step: 'magic-link-resent', sentEmail: 'you@example.com' })",
700
- glow_when: ds_glow.call("auth", step: "magic-link-resent") } do %>
701
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
702
- <div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-primary) 15%, transparent)">
703
- <svg class="w-4 h-4" style="color: var(--color-primary)" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
704
- </div>
705
- <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
706
- <span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
707
- </div>
708
- <% end %>
709
-
710
- <%= render layout: "style/modal_specimen", locals: {
711
- label: "Redirect (drain)",
712
- reference: %(the Auth "Redirect" step (style/modals/_auth, step 'redirect', drain CTA) — open with $store.dsModals.open('auth', { step: 'redirect', icon: '📍', title: 'One more step', message: 'Taking you there.', url: null })),
713
- open_expr: "$store.dsModals.open('auth', { step: 'redirect', icon: '📍', title: 'One more step', message: 'Taking you there.', url: null })",
714
- glow_when: ds_glow.call("auth", step: "redirect") } do %>
715
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
716
- <div class="text-2xl leading-none">📍</div>
717
- <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
718
- <span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
662
+ <span class="block h-1.5 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
663
+ <span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-primary); opacity: .7"></span>
664
+ <span class="block h-1.5 w-10 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
719
665
  </div>
720
666
  <% end %>
721
667
  </div>
@@ -1094,16 +1040,7 @@
1094
1040
  <% end %>
1095
1041
  The cards run in the order a player meets them &mdash; <strong>connect a
1096
1042
  wallet, prove it, spend from it</strong>: Connect wallet &rarr; the two
1097
- Sign Wallet states &rarr; Processing &rarr; success or error &rarr;
1098
- top-up &rarr; Entry confirmed. Inside that, <strong>Connect Wallet &rarr;
1099
- Processing on-chain transaction &rarr; On-chain success</strong> is a
1100
- <strong>walked flow</strong> and the live card <strong>glows</strong>,
1101
- following it as it advances (open Connect Wallet and pick a wallet to watch
1102
- it walk).
1103
- The <strong>Processing</strong> card is a load modal: its
1104
- <strong>success/error toggle</strong> chooses whether it resolves to success
1105
- or the error state, after the <strong>minimum-visible duration</strong>
1106
- (<code class="font-mono text-2xs">studio/modals/_load_convention</code>). The
1043
+ Sign Wallet states &rarr; Entry confirmed. The
1107
1044
  Connect-wallet picker ships its <strong>Phantom / Solflare / Backpack brand
1108
1045
  marks inline from the engine</strong>
1109
1046
  (<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
@@ -1223,95 +1160,6 @@
1223
1160
  </div>
1224
1161
  <% end %>
1225
1162
 
1226
- <%# 2. Processing — the load modal; the success/error toggle picks the resolution. %>
1227
- <%= render layout: "style/modal_specimen", locals: {
1228
- label: "Processing on-chain tx",
1229
- reference: %(the Web3 "Processing" on-chain load modal (style/modals/_onchain_tx via $store.dsSolanaModal) — holds the spinner at least MIN_LOAD_MS, then auto-resolves to success (or the error state when the toggle is set). Open with $store.dsModals.open('onchain-tx', { state: 'processing', demoResolve: true, demoError: false })),
1230
- card_data: "opts: { demoError: false }",
1231
- toggles: [{ model: "opts.demoError", label: "Resolve to error" }],
1232
- open_expr: "$store.dsModals.open('onchain-tx', { state: 'processing', title: 'Confirming on-chain', message: 'Waiting for the wallet signature…', demoResolve: true, demoError: opts.demoError })",
1233
- glow_when: ds_glow.call("onchain-tx", state: "processing"),
1234
- disabled: !web3_on, openable: true } do %>
1235
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
1236
- <div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
1237
- <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1238
- </div>
1239
- <% end %>
1240
-
1241
- <%# 3. On-chain success — the resolved success end-state (the walk's finish). %>
1242
- <%= render layout: "style/modal_specimen", locals: {
1243
- label: "On-chain success",
1244
- reference: %(the Web3 "On-chain success" state (style/modals/_onchain_tx + studio/modals/blocks/_onchain_success) — the resolved success card the walk lands on. Open with $store.dsModals.open('onchain-tx', { state: 'success', txSignature: '…', successTitle: 'Mint complete' })),
1245
- open_expr: "$store.dsModals.open('onchain-tx', { state: 'success', txSignature: '5xTrDemoSignature1234567890', successTitle: 'Mint complete', successSubtitle: 'Your on-chain action confirmed.', ctaLabel: 'View wallet', ctaHref: null })",
1246
- glow_when: ds_glow.call("onchain-tx", state: "success"),
1247
- disabled: !web3_on, openable: true } do %>
1248
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
1249
- <div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-success) 15%, transparent)">
1250
- <svg class="w-4 h-4" style="color: var(--color-success)" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
1251
- </div>
1252
- <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1253
- </div>
1254
- <% end %>
1255
-
1256
- <%# On-chain error — the resolved error end-state (the toggle's other branch). %>
1257
- <%= render layout: "style/modal_specimen", locals: {
1258
- label: "On-chain error",
1259
- reference: %(the Web3 "On-chain error" state (style/modals/_onchain_tx) — the resolved error card the Processing toggle resolves to. Open with $store.dsModals.open('onchain-tx', { state: 'error', title: "Couldn't confirm", errorMessage: 'The transaction was rejected. Give it another try.' })),
1260
- open_expr: "$store.dsModals.open('onchain-tx', { state: 'error', title: 'Could not confirm', errorMessage: 'The transaction was rejected. Give it another try.' })",
1261
- glow_when: ds_glow.call("onchain-tx", state: "error"),
1262
- disabled: !web3_on, openable: true } do %>
1263
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
1264
- <div class="text-2xl leading-none">⚠️</div>
1265
- <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1266
- </div>
1267
- <% end %>
1268
-
1269
- <%# ds-wallet-topup — the FIRST funds modal, and a safety fork. %>
1270
- <%= render layout: "style/modal_specimen", locals: {
1271
- label: "Top up wallet",
1272
- reference: %(the first funds modal a player meets (style/modals/_ds_wallet_topup) — turf-monster's modals/_wallet_topup, built here from blocks/_close_x and blocks/_rail_row. THE TOGGLE IS THE CARD: this is one modal with two faces, and the fork is a SAFETY one rather than a preference. A web2 viewer running with ENABLE_WEB2_USDC_ENTRY off cannot pay an entry with USDC at all, so pitching "Buy USDC" would sell them a thing that cannot do the job they came to do — for exactly that audience the primary rail becomes the entry token, and the balance line follows, because the currency it names has to be the one that can pay. Shown as ONE card with a toggle rather than two, because two would read as two products and the point is that a player never sees both. Open with $store.dsModals.open('ds-wallet-topup', { tokenFallback: false })),
1273
- card_data: "opts: { tokenFallback: false }",
1274
- toggles: [{ model: "opts.tokenFallback", label: "web2 kill-switch" }],
1275
- open_expr: "$store.dsModals.open('ds-wallet-topup', { tokenFallback: opts.tokenFallback })",
1276
- glow_when: ds_glow.call("ds-wallet-topup"),
1277
- disabled: !web3_on, openable: true } do %>
1278
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
1279
- <span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1280
- <span class="flex items-center gap-1.5 rounded-md p-1.5" style="border: 2px solid var(--color-primary)">
1281
- <span class="block h-4 w-4 rounded" style="background: var(--color-cta)"></span>
1282
- <span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .3"></span>
1283
- </span>
1284
- <span class="block h-1.5 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1285
- </div>
1286
- <% end %>
1287
-
1288
- <%# ds-onramp-hub — every rail at once, which is where ranking earns its keep. %>
1289
- <%= render layout: "style/modal_specimen", locals: {
1290
- label: "Add funds hub",
1291
- reference: %(the full Add Funds hub (style/modals/_ds_onramp_hub) — turf-monster's modals/_onramp_hub, and the card blocks/_rail_row was extracted FOR: turf draws these six by hand today, which is six chances for the seventh to drift. Six rails is enough that "which one do I press" becomes a real question, and a hub where every rail carries equal weight answers it by asking the person to compare payment processors — not a question anyone can answer about their own life. Exactly ONE rail is :primary and the other five defer to it. PayPal and Venmo show the THIRD shape, :soon — announced before they are wired, so plain divs rather than disabled buttons, with a badge where the chevron would be, because even a dimmed chevron still promises somewhere to go. Open with $store.dsModals.open('ds-onramp-hub')),
1292
- open_expr: "$store.dsModals.open('ds-onramp-hub')",
1293
- glow_when: ds_glow.call("ds-onramp-hub"),
1294
- disabled: !web3_on, openable: true } do %>
1295
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-3 space-y-1.5">
1296
- <span class="flex items-center gap-1.5 rounded p-1" style="border: 2px solid var(--color-primary)">
1297
- <span class="block h-3 w-3 rounded-sm" style="background: var(--color-cta)"></span>
1298
- <span class="block h-1 flex-1 rounded" style="background: var(--color-text); opacity: .3"></span>
1299
- </span>
1300
- <% 3.times do %>
1301
- <span class="flex items-center gap-1.5 rounded p-1 border border-subtle">
1302
- <span class="block h-3 w-3 rounded-sm" style="background: var(--color-text); opacity: .25"></span>
1303
- <span class="block h-1 flex-1 rounded" style="background: var(--color-text); opacity: .18"></span>
1304
- </span>
1305
- <% end %>
1306
- <%# the two :soon rails, dimmed %>
1307
- <% 2.times do %>
1308
- <span class="flex items-center gap-1.5 rounded p-1 border border-subtle" style="opacity: .5">
1309
- <span class="block h-3 w-3 rounded-sm" style="background: var(--color-text); opacity: .25"></span>
1310
- <span class="block h-1 flex-1 rounded" style="background: var(--color-text); opacity: .12"></span>
1311
- </span>
1312
- <% end %>
1313
- </div>
1314
- <% end %>
1315
1163
 
1316
1164
  <%# Entry confirmed — the GENERIC web3 celebration (tx link + heading + drain
1317
1165
  CTA). seedsEarned: 0 so no seeds bar even where leveling is on: this card
@@ -1358,14 +1206,25 @@
1358
1206
  about who the account is. Both are engine primitives gated by
1359
1207
  <code class="font-mono text-2xs">Studio.feature?(:age_gate)</code>, and their
1360
1208
  minimum age, jurisdiction label, endpoint, and legal copy are all
1361
- <strong>app-supplied</strong> (the engine hardcodes no policy). The entry flow is
1362
- a <strong>walked sequence</strong> &mdash; <strong>Entry tokens &rarr; Payment
1363
- processing &rarr; Entry Tokens Minted &rarr; Contest enter processing &rarr;
1364
- Contest entered</strong> &mdash; and the live card <strong>glows</strong>,
1365
- following the step machine (the two load steps honor the same min-duration
1366
- convention as Web3 above).
1209
+ <strong>app-supplied</strong> (the engine hardcodes no policy).
1210
+ </p>
1211
+ <p class="text-muted text-sm">
1212
+ <strong>The entry flow itself has no specimen here, on purpose.</strong>
1213
+ Packs, pricing, payment rails and every on-chain write are the app&rsquo;s
1214
+ &mdash; the engine owns none of them, and the walked purchase sequence that
1215
+ used to sit in this section was a <strong>mirror</strong> of turf-monster&rsquo;s
1216
+ cards. It drifted, and it demonstrated a payment face that had stopped
1217
+ rendering in production. It was retired on 2026-09-09; turf-monster cards
1218
+ the live flow on its own section of this guide.
1219
+ <strong>What stays is the map below</strong>, because the shape of the flow
1220
+ is the part an app should not have to rediscover.
1367
1221
  </p>
1368
- <%# The web2/web3 finding — a real divergence, stated honestly, not invented. %>
1222
+ <%# The web2/web3 finding — a real divergence, stated honestly, not invented.
1223
+ THIS BOX IS THE SURVIVING HALF of style/modals/_entry_tokens, whose header
1224
+ carried the same model in a comment. When that mirror was deleted the
1225
+ model was folded in HERE, where it renders instead of hiding in a file
1226
+ nobody opens — the two sentences about what a token IS, and about who
1227
+ signs the mint, came across in that move. %>
1369
1228
  <div class="mt-2 rounded-lg border border-subtle bg-surface p-3 text-xs text-secondary leading-snug">
1370
1229
  <p class="text-heading font-semibold mb-1">Web2 vs web3 — the honest map</p>
1371
1230
  <p class="mb-1">
@@ -1377,11 +1236,17 @@
1377
1236
  <strong>mints no token</strong> &mdash; the mint&nbsp;&rarr;&nbsp;consume prelude
1378
1237
  is web2-only.
1379
1238
  </p>
1239
+ <p class="mb-1">
1240
+ An entry token is a <strong>program-owned prepaid entry credit</strong>
1241
+ recorded on-chain as its own account &mdash; <strong>not</strong> a fungible
1242
+ SPL token sitting in the player&rsquo;s wallet, which is the wrong mental
1243
+ model to carry into the code. And the mint is <strong>server-signed once
1244
+ the payment clears</strong>, never on the buyer&rsquo;s click: the two
1245
+ on-chain moments a web2 entry has are that mint and the later consume.
1246
+ </p>
1380
1247
  <p>
1381
1248
  Both create the <strong>same on-chain Entry PDA</strong> and resolve to the
1382
- <strong>same “Entry Confirmed” card</strong>. The walk below demos the web2
1383
- path end-to-end; web3 skips the mint and joins at
1384
- <strong>enter&nbsp;&rarr;&nbsp;entered</strong>.
1249
+ <strong>same “Entry Confirmed” card</strong>.
1385
1250
  </p>
1386
1251
  </div>
1387
1252
  </div>
@@ -1391,79 +1256,11 @@
1391
1256
  them by the moment they happen to be asked. ENFORCEMENT still lives
1392
1257
  here: the bar is checked at contest entry, which is what this section is
1393
1258
  named for. The demo walk is unbroken — confirming the birthday card
1394
- still advances to Entry tokens below, via the page-level 'age-verified'
1395
- wiring, which the rename did NOT touch precisely because consuming apps
1396
- already listen on it. %>
1397
-
1398
- <%# 1. Entry tokens — the picker (walk's entry). Picking a pack advances. %>
1399
- <%= render layout: "style/modal_specimen", locals: {
1400
- label: "Entry tokens",
1401
- reference: %(the "Entry tokens" picker (app-specific — documented composing studio/modals/blocks chrome). The packs, pricing, rails, and on-chain mint stay app-owned. Picking a pack walks Payment processing -> Minted -> enter -> entered. Open with $store.dsModals.open('entry-tokens', { step: 'picker' })),
1402
- open_expr: "$store.dsModals.open('entry-tokens', { step: 'picker' })",
1403
- glow_when: ds_glow.call("entry-tokens", step: "picker", step_default: "picker") } do %>
1404
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
1405
- <span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1406
- <span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
1407
- <span class="block h-6 w-full rounded border" style="border-color: var(--color-primary)"></span>
1408
- <span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
1409
- </div>
1410
- <% end %>
1259
+ still resumes, via the page-level 'age-verified' wiring, which no rename
1260
+ and no deletion has touched, precisely because consuming apps listen on
1261
+ it. What it resumes TO is now Entry confirmed (Web3 above): the old
1262
+ target was one of the retired mirrors. %>
1411
1263
 
1412
- <%# 2. Payment processing — the mint-at-purchase load step (web2). %>
1413
- <%= render layout: "style/modal_specimen", locals: {
1414
- label: "Payment processing",
1415
- reference: %(the "Payment processing" step (entry-tokens step 'confirming') — "Confirming your purchase…" over "Payment cleared — a server job mints one prepaid entry credit per token on-chain." A load modal; honors the min-duration convention then auto-advances to Entry Tokens Minted. Open with $store.dsModals.open('entry-tokens', { step: 'confirming', mintedCount: 3, mintedBalance: '3' })),
1416
- open_expr: "$store.dsModals.open('entry-tokens', { step: 'confirming', mintedCount: 3, mintedBalance: '3' })",
1417
- glow_when: ds_glow.call("entry-tokens", step: "confirming", step_default: "picker") } do %>
1418
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
1419
- <div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
1420
- <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1421
- <span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
1422
- </div>
1423
- <% end %>
1424
-
1425
- <%# 3. Entry Tokens Minted — ready to consume at entry (hold to confirm). %>
1426
- <%= render layout: "style/modal_specimen", locals: {
1427
- label: "Entry Tokens Minted",
1428
- reference: %(the "Entry Tokens Minted" step (entry-tokens step 'minted') — "You now have N available." Holding to confirm consumes one token on-chain to create the contest entry (advances to Contest enter processing). Open with $store.dsModals.open('entry-tokens', { step: 'minted', mintedCount: 3, mintedBalance: '3' })),
1429
- open_expr: "$store.dsModals.open('entry-tokens', { step: 'minted', mintedCount: 3, mintedBalance: '3' })",
1430
- glow_when: ds_glow.call("entry-tokens", step: "minted", step_default: "picker") } do %>
1431
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
1432
- <div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-primary) 15%, transparent)">
1433
- <span class="text-sm font-bold" style="color: var(--color-primary)">3</span>
1434
- </div>
1435
- <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1436
- <span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
1437
- </div>
1438
- <% end %>
1439
-
1440
- <%# 4. Contest enter processing — the on-chain consume load step. %>
1441
- <%= render layout: "style/modal_specimen", locals: {
1442
- label: "Contest enter processing",
1443
- reference: %(the "Contest enter processing" step (entry-tokens step 'entering') — "Confirming your entry…" over "Consuming one token on-chain to create your contest entry." A load modal; honors the min-duration convention then auto-advances to Contest entered. Open with $store.dsModals.open('entry-tokens', { step: 'entering' })),
1444
- open_expr: "$store.dsModals.open('entry-tokens', { step: 'entering' })",
1445
- glow_when: ds_glow.call("entry-tokens", step: "entering", step_default: "picker") } do %>
1446
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
1447
- <div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
1448
- <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1449
- <span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
1450
- </div>
1451
- <% end %>
1452
-
1453
- <%# 5. Contest entered — the shared Entry Confirmed finish (web2 + web3 converge). %>
1454
- <%= render layout: "style/modal_specimen", locals: {
1455
- label: "Contest entered",
1456
- reference: %(the "Contest entered" success (entry-tokens step 'entered') — "Good Luck / Entry Confirmed" with the branded on-chain tx link and a Contest Lobby CTA. The SAME finish both web2 and web3 entries converge on. Open with $store.dsModals.open('entry-tokens', { step: 'entered', txSignature: '…' })),
1457
- open_expr: "$store.dsModals.open('entry-tokens', { step: 'entered', txSignature: '5xTrDemoEntrySignature1234567890abc' })",
1458
- glow_when: ds_glow.call("entry-tokens", step: "entered", step_default: "picker") } do %>
1459
- <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
1460
- <div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-success) 15%, transparent)">
1461
- <svg class="w-4 h-4" style="color: var(--color-success)" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
1462
- </div>
1463
- <span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1464
- <span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
1465
- </div>
1466
- <% end %>
1467
1264
 
1468
1265
  <%# it-begins — the moment a contest locks. The DRAINING CTA is the whole
1469
1266
  card: it interrupts someone mid-page at a moment they did not choose,
@@ -1788,22 +1585,32 @@
1788
1585
  <h3 class="text-xl font-bold text-heading">Stack behaviour</h3>
1789
1586
  <p class="text-muted text-sm">
1790
1587
  The engine modal-stack mechanics, poked at directly on
1791
- <code class="font-mono text-2xs">$store.dsModals</code>: the on-chain TX card states,
1792
- dismissibility, the minimum-visible-duration floor, and LIFO stacking.
1588
+ <code class="font-mono text-2xs">$store.dsModals</code>:
1589
+ <code class="font-mono text-2xs">advance()</code>, dismissibility, the
1590
+ minimum-visible-duration floor, and LIFO stacking. They all run on one
1591
+ throwaway vehicle
1592
+ (<code class="font-mono text-2xs">style/modals/_ds_stack_demo</code>) built
1593
+ from engine blocks &mdash; <strong>the mechanics are the subject, the card
1594
+ is not</strong>. Until 2026-09-09 they ran on a copy of turf-monster&rsquo;s
1595
+ on-chain card, which is how six unrelated demos came to depend on one
1596
+ consumer&rsquo;s markup.
1793
1597
  </p>
1794
1598
  </div>
1795
1599
 
1796
1600
  <div class="grid gap-4 sm:grid-cols-2">
1797
- <%# The three states of style/modals/_onchain_tx. The two static buttons open
1798
- the card AT a state; the two timed buttons drive the transition through
1799
- the store's advance(), which is the in-flow step primitive. %>
1601
+ <%# advance() the in-flow step primitive. The static buttons open the
1602
+ vehicle AT a face; the timed pair transitions a LIVE card, which is the
1603
+ whole distinction: advance() patches props on the stack entry that is
1604
+ already open, so the card's x-data survives, while swap() replaces the
1605
+ entry and rebuilds it. Three ids could not show this — which is why the
1606
+ vehicle carries three faces on one. %>
1800
1607
  <div class="card p-5 space-y-3">
1801
- <h4 class="text-heading font-semibold text-sm">On-chain TX states</h4>
1608
+ <h4 class="text-heading font-semibold text-sm">In-flow advance()</h4>
1802
1609
  <p class="text-secondary text-xs">
1803
- Engine <code class="font-mono text-2xs">blocks/_card_header</code> and
1804
- <code class="font-mono text-2xs">blocks/_onchain_success</code>. The timed pair
1805
- transitions a live card with
1806
- <code class="font-mono text-2xs">advance()</code>.
1610
+ <code class="font-mono text-2xs">advance(patch)</code> patches the OPEN
1611
+ entry rather than pushing a new one, so the card&rsquo;s Alpine scope
1612
+ survives the transition. Watch the timed pair change a card that never
1613
+ left the stack.
1807
1614
  </p>
1808
1615
  <div class="flex flex-wrap gap-2">
1809
1616
  <button type="button" class="btn btn-outline btn-sm"
@@ -1876,51 +1683,54 @@
1876
1683
  THE STORE IS dsModals. turf-monster's version of this page drove roughly
1877
1684
  half of these through its legacy solana-modal compatibility proxy, a store
1878
1685
  this engine does not ship; every demo here goes through the page-scoped
1879
- store and the onchain-tx specimen directly instead. The exact spelling of
1686
+ store and the ds-stack-demo vehicle directly instead. The exact spelling of
1880
1687
  that legacy store name is deliberately absent from this file, because
1881
1688
  style_guide_modal_simulator_test scans the source for it. %>
1882
1689
  <script>
1883
1690
  window.dsModalDemos = (function () {
1884
- var SAMPLE_TX = '5xTrDemoSignature1234567890abcXYZ';
1691
+ var VEHICLE = 'ds-stack-demo';
1885
1692
 
1886
1693
  function store() { return Alpine.store('dsModals'); }
1887
1694
 
1888
- // Open the onchain-tx specimen. The defaults mirror a real pending
1889
- // transaction, INCLUDING dismissible: false — the demos that want an
1695
+ // Open the stack-mechanics vehicle. The defaults model a real pending
1696
+ // operation, INCLUDING dismissible: false — the demos that want an
1890
1697
  // escapable card override it explicitly.
1891
- function openTx(props) {
1892
- store().open('onchain-tx', Object.assign({
1698
+ //
1699
+ // THE VEHICLE IS DELIBERATELY GENERIC. These demos ran on the onchain-tx
1700
+ // specimen until 2026-09-09, which made every one of them depend on a copy
1701
+ // of turf-monster's card; retiring that copy would have taken the whole
1702
+ // section with it. style/modals/_ds_stack_demo belongs to this page and to
1703
+ // nothing that ships.
1704
+ function openDemo(props) {
1705
+ store().open(VEHICLE, Object.assign({
1893
1706
  state: 'processing',
1894
1707
  title: 'Submitting entry',
1895
- message: 'Awaiting wallet signature',
1708
+ message: 'Awaiting confirmation',
1896
1709
  dismissible: false
1897
1710
  }, props || {}));
1898
1711
  }
1899
1712
 
1900
1713
  // In-flow state change on the card that is already open. advance() patches
1901
- // props WITHOUT replacing the stack entry, so the specimen's x-data scope
1902
- // survives the transition. Guarded to a still-open onchain-tx card: a timer
1714
+ // props WITHOUT replacing the stack entry, so the vehicle's x-data scope
1715
+ // survives the transition. Guarded to a still-open vehicle card: a timer
1903
1716
  // that fires after the user closed it must do nothing rather than patch
1904
1717
  // whatever card happens to be on top now.
1905
- function advanceTx(patch) {
1718
+ function advanceDemo(patch) {
1906
1719
  var cur = store().current();
1907
- if (!cur || cur.id !== 'onchain-tx' || cur._closing) return;
1720
+ if (!cur || cur.id !== VEHICLE || cur._closing) return;
1908
1721
  store().advance(patch);
1909
1722
  }
1910
1723
 
1911
1724
  var SUCCESS = {
1912
1725
  state: 'success',
1913
- txSignature: SAMPLE_TX,
1914
- successTitle: 'Entry confirmed',
1915
- successSubtitle: 'Your entry is confirmed on-chain.',
1916
- ctaLabel: 'View wallet',
1917
- ctaHref: null,
1726
+ title: 'Entry confirmed',
1727
+ message: 'Resolved in place — this is the same stack entry you opened.',
1918
1728
  dismissible: true
1919
1729
  };
1920
1730
  var FAILURE = {
1921
1731
  state: 'error',
1922
1732
  title: 'Submission failed',
1923
- errorMessage: 'Transaction simulation failed: insufficient SOL for rent.',
1733
+ message: 'The error face of that same entry, patched in by advance().',
1924
1734
  dismissible: true
1925
1735
  };
1926
1736
 
@@ -2017,34 +1827,34 @@
2017
1827
 
2018
1828
  return {
2019
1829
  // Stack behaviour
2020
- processing: function () { openTx({}); },
2021
- success: function () { openTx(SUCCESS); },
2022
- error: function () { openTx(FAILURE); },
1830
+ processing: function () { openDemo({}); },
1831
+ success: function () { openDemo(SUCCESS); },
1832
+ error: function () { openDemo(FAILURE); },
2023
1833
  processThenSuccess: function () {
2024
- openTx({ message: 'Awaiting RPC confirmation' });
2025
- setTimeout(function () { advanceTx(SUCCESS); }, 3000);
1834
+ openDemo({ message: 'Awaiting RPC confirmation' });
1835
+ setTimeout(function () { advanceDemo(SUCCESS); }, 3000);
2026
1836
  },
2027
1837
  processThenError: function () {
2028
- openTx({ message: 'Awaiting RPC confirmation' });
2029
- setTimeout(function () { advanceTx(FAILURE); }, 3000);
1838
+ openDemo({ message: 'Awaiting RPC confirmation' });
1839
+ setTimeout(function () { advanceDemo(FAILURE); }, 3000);
2030
1840
  },
2031
1841
  dismissible: function () {
2032
- openTx({
1842
+ openDemo({
2033
1843
  dismissible: true,
2034
1844
  title: 'Dismissible processing',
2035
1845
  message: 'Escape and click-outside both close this one.'
2036
1846
  });
2037
1847
  },
2038
1848
  fastWithHold: function () {
2039
- openTx({ title: 'Fast operation', message: 'Holding the spinner for at least 1500ms' });
2040
- window.StudioModals.holdAtLeast(1500).then(function () { advanceTx(SUCCESS); });
1849
+ openDemo({ title: 'Fast operation', message: 'Holding the spinner for at least 1500ms' });
1850
+ window.StudioModals.holdAtLeast(1500).then(function () { advanceDemo(SUCCESS); });
2041
1851
  },
2042
1852
  fastNoHold: function () {
2043
- openTx({ title: 'Fast operation', message: 'No hold, so this flashes past' });
2044
- setTimeout(function () { advanceTx(SUCCESS); }, 50);
1853
+ openDemo({ title: 'Fast operation', message: 'No hold, so this flashes past' });
1854
+ setTimeout(function () { advanceDemo(SUCCESS); }, 50);
2045
1855
  },
2046
1856
  stackTwo: function () {
2047
- openTx({
1857
+ openDemo({
2048
1858
  dismissible: true,
2049
1859
  title: 'Bottom of the stack',
2050
1860
  message: 'A second card is about to push in front of this one.'