@open-slot-ui/pixi 0.10.0 → 0.11.1

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.
package/dist/index.cjs CHANGED
@@ -3988,101 +3988,16 @@ var PanelBodyView = class extends ControlView {
3988
3988
  super.dispose();
3989
3989
  }
3990
3990
  };
3991
- var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3992
- var rich = (s) => esc(s).replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
3993
- function statGridHtml(items, tr) {
3994
- const rows = items.map((it) => `<div><dt>${esc(tr(it.label))}</dt><dd>${esc(tr(it.value))}</dd></div>`).join("");
3995
- return `<dl class="ohm-stats">${rows}</dl>`;
3996
- }
3997
- function renderBlocks(blocks, tr, facts) {
3998
- const out = [];
3999
- for (const b of blocks) {
4000
- switch (b.kind) {
4001
- case "text":
4002
- out.push(`<p>${rich(tr(b.text))}</p>`);
4003
- break;
4004
- case "heading":
4005
- out.push(`<div class="ohm-sec"><span>${esc(tr(b.text))}</span></div>`);
4006
- break;
4007
- case "subheading":
4008
- out.push(`<h4 class="ohm-subh">${esc(tr(b.text))}</h4>`);
4009
- break;
4010
- case "legal":
4011
- out.push(`<p class="ohm-legal">${rich(tr(b.text))}</p>`);
4012
- break;
4013
- case "divider":
4014
- out.push('<hr class="ohm-hr">');
4015
- break;
4016
- case "image":
4017
- out.push(`<img class="ohm-feature" alt="${esc(tr(b.alt ?? ""))}" src="${b.src}" loading="lazy">`);
4018
- break;
4019
- case "media": {
4020
- const img = `<img alt="${esc(tr(b.alt ?? ""))}" src="${b.src}" loading="lazy">`;
4021
- const body = `<div class="ohm-media-body">${b.title ? `<h4>${esc(tr(b.title))}</h4>` : ""}<p>${rich(tr(b.text))}</p></div>`;
4022
- out.push(`<div class="ohm-media ohm-media--${b.side ?? "left"}">${img}${body}</div>`);
4023
- break;
4024
- }
4025
- case "cards": {
4026
- const cards = b.items.map((it) => `<div class="ohm-fcard">${it.icon ? `<img src="${it.icon}" alt="" loading="lazy">` : ""}<h5>${esc(tr(it.title))}</h5>${it.text ? `<p>${rich(tr(it.text))}</p>` : ""}</div>`).join("");
4027
- out.push(`<div class="ohm-cards">${cards}</div>`);
4028
- break;
4029
- }
4030
- case "paytable":
4031
- out.push(`<div class="ohm-grid">${renderPaytable(b.rows, tr)}</div>`);
4032
- break;
4033
- case "table": {
4034
- const head = b.columns?.length ? `<thead><tr>${b.columns.map((c) => `<th>${esc(tr(c))}</th>`).join("")}</tr></thead>` : "";
4035
- const body = b.rows.map((r) => `<tr>${r.map((c) => `<td>${esc(tr(c))}</td>`).join("")}</tr>`).join("");
4036
- out.push(`<table class="ohm-table">${head}<tbody>${body}</tbody></table>`);
4037
- break;
4038
- }
4039
- case "stat-grid":
4040
- out.push(statGridHtml(b.items, tr));
4041
- break;
4042
- // The AUTO per-mode RTP / Max-win grid — rendered straight from the declared
4043
- // game facts (+ any host extras), so the table can never drift from the config.
4044
- // `modeStatsItems` localizes the label PARTS itself ("Max win" · the mode name),
4045
- // so the grid renderer gets identity-tr — no double translation.
4046
- case "mode-stats": {
4047
- const extras = (b.extras ?? []).map((e) => ({ label: tr(e.label), value: tr(e.value) }));
4048
- out.push(statGridHtml([...core.modeStatsItems(facts, tr), ...extras], (s) => s));
4049
- break;
4050
- }
4051
- case "steps": {
4052
- const items = b.items.map((s) => `<li>${rich(tr(s))}</li>`).join("");
4053
- out.push(b.ordered ? `<ol class="ohm-steps">${items}</ol>` : `<ul class="ohm-steps">${items}</ul>`);
4054
- break;
4055
- }
4056
- case "callout":
4057
- out.push(`<div class="ohm-callout ohm-callout--${b.tone ?? "info"}">${b.title ? `<b>${esc(tr(b.title))}</b>` : ""}<p>${rich(tr(b.text))}</p></div>`);
4058
- break;
4059
- case "group":
4060
- out.push(`<div class="ohm-group">${b.title ? `<h4 class="ohm-subh">${esc(tr(b.title))}</h4>` : ""}${renderBlocks(b.children, tr)}</div>`);
4061
- break;
4062
- }
4063
- }
4064
- return out.join("\n");
4065
- }
4066
- function renderPaytable(rows, tr) {
4067
- return rows.map((r) => {
4068
- const icon = r.icon ? `<img class="ohm-symimg" src="${r.icon}" alt="" loading="lazy">` : `<span class="ohm-emoji">${esc(tr(r.symbol ?? ""))}</span>`;
4069
- const lines = r.payouts.split("\n").map((line) => {
4070
- const i = line.indexOf(":");
4071
- return i >= 0 ? `<div><b>${esc(line.slice(0, i))}</b><span>${esc(line.slice(i + 1))}</span></div>` : `<div><span>${esc(line)}</span></div>`;
4072
- }).join("");
4073
- return `<div class="ohm-sym">${icon}<div class="ohm-pay">${lines}</div></div>`;
4074
- }).join("");
4075
- }
4076
3991
  function renderSettingBlock(b, tr) {
4077
- const hint = "hint" in b && b.hint ? `<div class="ohm-hint">${esc(tr(b.hint))}</div>` : "";
4078
- const label = "label" in b && b.label ? esc(tr(b.label)) : "";
3992
+ const hint = "hint" in b && b.hint ? `<div class="ohm-hint">${core.escapeHtml(tr(b.hint))}</div>` : "";
3993
+ const label = "label" in b && b.label ? core.escapeHtml(tr(b.label)) : "";
4079
3994
  switch (b.kind) {
4080
3995
  case "toggle":
4081
3996
  return `<div class="ohm-setting"><label class="ohm-row ohm-check"><span>${label}</span><span class="ohm-ctl"><input data-set="${b.id}" type="checkbox"${b.on ? " checked" : ""}></span></label>${hint}</div>`;
4082
3997
  case "slider":
4083
3998
  return `<div class="ohm-setting"><label class="ohm-row"><span>${label}</span><input data-set="${b.id}" type="range" min="0" max="1" step="0.01" value="${b.initial ?? 1}"></label>${hint}</div>`;
4084
3999
  case "select": {
4085
- const opts = b.options.map((o, i) => `<option value="${esc(o.value)}"${i === (b.index ?? 0) ? " selected" : ""}>${esc(tr(o.label))}</option>`).join("");
4000
+ const opts = b.options.map((o, i) => `<option value="${core.escapeHtml(o.value)}"${i === (b.index ?? 0) ? " selected" : ""}>${core.escapeHtml(tr(o.label))}</option>`).join("");
4086
4001
  return `<div class="ohm-setting"><label class="ohm-row"><span>${label}</span><select data-set="${b.id}">${opts}</select></label>${hint}</div>`;
4087
4002
  }
4088
4003
  default:
@@ -4095,25 +4010,16 @@ function mountInfoMenu(app, ui) {
4095
4010
  const disposers = [];
4096
4011
  const host2 = document.createElement("div");
4097
4012
  host2.className = "ohm-root";
4098
- const vars = {
4099
- "--accent": "#d99000",
4100
- "--accent-text": "#1a1200",
4101
- "--surface": "#ffffff",
4102
- "--surface-alt": "#eef1f6",
4103
- "--text": "#181b20",
4104
- "--text-dim": "#5b6472",
4105
- "--card-radius": "8px",
4106
- "--font": ui.theme.type.family
4107
- };
4013
+ const vars = { ...core.INFO_MENU_VARS, "--font": ui.theme.type.family };
4108
4014
  for (const [k, v] of Object.entries(vars)) host2.style.setProperty(k, v);
4109
4015
  const locales = ui.spec?.locale ? Array.from(/* @__PURE__ */ new Set([ui.spec.locale.locale, ...Object.keys(ui.spec.locale.messages)])) : [];
4110
- const langRow = locales.length > 1 ? `<div class="ohm-setting"><label class="ohm-row"><span>${tr("Language")}</span><select id="ohm-lang">${locales.map((c) => `<option value="${c}"${c === ui.locale.get() ? " selected" : ""}>${esc(core.LOCALE_LABELS[c] ?? c)}</option>`).join("")}</select></label></div>` : "";
4016
+ const langRow = locales.length > 1 ? `<div class="ohm-setting"><label class="ohm-row"><span>${tr("Language")}</span><select id="ohm-lang">${locales.map((c) => `<option value="${c}"${c === ui.locale.get() ? " selected" : ""}>${core.escapeHtml(core.LOCALE_LABELS[c] ?? c)}</option>`).join("")}</select></label></div>` : "";
4111
4017
  const turbo = ui.turbo;
4112
4018
  const cap = (m) => m.charAt(0).toUpperCase() + m.slice(1);
4113
- const turboCtl = turbo.modeCount <= 2 ? `<span class="ohm-ctl"><input id="ohm-turbo" type="checkbox"></span>` : `<span class="ohm-ctl"><div class="ohm-segmented" id="ohm-turbo-seg">${turbo.modes.map((m, i) => `<button class="ohm-seg" data-i="${i}">${esc(tr(cap(m)))}</button>`).join("")}</div></span>`;
4019
+ const turboCtl = turbo.modeCount <= 2 ? `<span class="ohm-ctl"><input id="ohm-turbo" type="checkbox"></span>` : `<span class="ohm-ctl"><div class="ohm-segmented" id="ohm-turbo-seg">${turbo.modes.map((m, i) => `<button class="ohm-seg" data-i="${i}">${core.escapeHtml(tr(cap(m)))}</button>`).join("")}</div></span>`;
4114
4020
  const customSettings = (menu.settings ?? []).map((b) => renderSettingBlock(b, tr)).join("");
4115
4021
  const soundMode = menu.sound ?? "sliders";
4116
- const volRow = (id, label, hint, val) => `<div class="ohm-setting"><label class="ohm-row"><span>${esc(tr(label))}</span><input class="ohm-slider" data-vol="${id}" type="range" min="0" max="1" step="0.01" value="${val}"></label><div class="ohm-hint">${esc(tr(hint))}</div></div>`;
4022
+ const volRow = (id, label, hint, val) => `<div class="ohm-setting"><label class="ohm-row"><span>${core.escapeHtml(tr(label))}</span><input class="ohm-slider" data-vol="${id}" type="range" min="0" max="1" step="0.01" value="${val}"></label><div class="ohm-hint">${core.escapeHtml(tr(hint))}</div></div>`;
4117
4023
  const soundSliders = soundMode === "master" ? volRow("master", "Volume", "Adjust the overall game volume.", ui.sfxSlider.value.get()) : soundMode === "sliders" ? volRow("music", "Music", "Adjust the background music volume.", ui.musicSlider.value.get()) + volRow("sfx", "Effects", "Adjust the sound-effects volume.", ui.sfxSlider.value.get()) : "";
4118
4024
  const rulesTr = () => {
4119
4025
  const vars2 = core.factsVars(ui.facts.get(), { "game.name": ui.gameInfo.name ?? "", "game.version": ui.gameInfo.version ?? "" });
@@ -4121,25 +4027,16 @@ function mountInfoMenu(app, ui) {
4121
4027
  };
4122
4028
  const auditCardHtml = (trr) => {
4123
4029
  if (!menu.rules?.length) return "";
4124
- const issues = core.auditRules(ui.facts.get(), menu.rules, { resolve: trr });
4125
- if (!issues.length) return "";
4126
- const req = issues.filter((i) => i.level === "required");
4127
- const rec = issues.filter((i) => i.level === "recommended");
4128
- const list = (items) => `<ul>${items.map((i) => `<li>${esc(tr(i.message))}</li>`).join("")}</ul>`;
4129
- return `<div class="ohm-audit" role="alert">
4130
- <div class="ohm-audit-title">\u26A0 ${esc(tr("openui.rulesAudit.title"))}</div>
4131
- ${req.length ? `<div class="ohm-audit-sub">${esc(tr("openui.rulesAudit.required"))}</div>${list(req)}` : ""}
4132
- ${rec.length ? `<div class="ohm-audit-sub ohm-audit-sub--rec">${esc(tr("openui.rulesAudit.recommended"))}</div><div class="ohm-audit-rec">${list(rec)}</div>` : ""}
4133
- </div>`;
4030
+ return core.renderRulesAuditHtml(core.auditRules(ui.facts.get(), menu.rules, { resolve: trr }), tr);
4134
4031
  };
4135
4032
  const rulesInnerHtml = () => {
4136
4033
  if (!menu.rules) return "";
4137
4034
  const trr = rulesTr();
4138
- return auditCardHtml(trr) + renderBlocks(menu.rules, trr, ui.facts.get());
4035
+ return auditCardHtml(trr) + core.renderBlocksHtml(menu.rules, trr, ui.facts.get());
4139
4036
  };
4140
- const paytableHtml = menu.paytable ? renderBlocks(menu.paytable, tr, ui.facts.get()) : "";
4037
+ const paytableHtml = menu.paytable ? core.renderBlocksHtml(menu.paytable, tr, ui.facts.get()) : "";
4141
4038
  const rulesHtml = rulesInnerHtml();
4142
- const banner = menu.banner ? `<img class="ohm-logo" alt="" src="${menu.banner.src}"${menu.banner.width ? ` width="${menu.banner.width}"` : ""}${menu.banner.height ? ` height="${menu.banner.height}"` : ""}>` : ui.gameInfo.name ? `<h1 class="ohm-logo-text">${esc(ui.gameInfo.name)}</h1>` : "";
4039
+ const banner = menu.banner ? `<img class="ohm-logo" alt="" src="${menu.banner.src}"${menu.banner.width ? ` width="${menu.banner.width}"` : ""}${menu.banner.height ? ` height="${menu.banner.height}"` : ""}>` : ui.gameInfo.name ? `<h1 class="ohm-logo-text">${core.escapeHtml(ui.gameInfo.name)}</h1>` : "";
4143
4040
  host2.innerHTML = `
4144
4041
  <div class="ohm-backdrop" data-close></div>
4145
4042
  <button class="ohm-x" data-close aria-label="Close">\u2715</button>
@@ -4157,7 +4054,7 @@ function mountInfoMenu(app, ui) {
4157
4054
  </div>
4158
4055
  </div>`;
4159
4056
  const style = document.createElement("style");
4160
- style.textContent = OHM_CSS;
4057
+ style.textContent = core.INFO_MENU_CSS;
4161
4058
  host2.appendChild(style);
4162
4059
  document.body.appendChild(host2);
4163
4060
  const $ = (sel) => host2.querySelector(sel);
@@ -4247,91 +4144,6 @@ function mountInfoMenu(app, ui) {
4247
4144
  host2.remove();
4248
4145
  };
4249
4146
  }
4250
- var OHM_CSS = `
4251
- .ohm-root { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; font-family: var(--font); opacity: 0; pointer-events: none; transition: opacity .18s ease; }
4252
- .ohm-root.open { opacity: 1; pointer-events: auto; }
4253
- .ohm-backdrop { position: absolute; inset: 0; background: rgba(8,6,4,0); backdrop-filter: blur(0px) saturate(1); -webkit-backdrop-filter: blur(0px) saturate(1); transition: background .4s ease, backdrop-filter .4s ease, -webkit-backdrop-filter .4s ease; }
4254
- .ohm-root.open .ohm-backdrop { background: rgba(8,6,4,.34); backdrop-filter: blur(6px) saturate(1.1); -webkit-backdrop-filter: blur(6px) saturate(1.1); }
4255
- .ohm-card { position: relative; width: min(92%, 1100px); max-height: 86vh; display: flex; flex-direction: column; background: var(--surface); color: var(--text); border: 1.5px solid #000; border-radius: var(--card-radius); box-shadow: 0 30px 80px rgba(0,0,0,.5); overflow: hidden; transform: translateY(8px) scale(.99); transition: transform .18s ease; }
4256
- .ohm-root.open .ohm-card { transform: none; }
4257
- .ohm-x { position: absolute; top: 18px; right: 22px; width: 46px; height: 46px; border-radius: 999px; border: 0; background: rgba(18,14,10,.82); color: #fff; font-size: 18px; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 18px rgba(0,0,0,.45); z-index: 2; transition: transform .12s, background .12s; }
4258
- .ohm-x:hover { transform: scale(1.08); background: rgba(18,14,10,.95); }
4259
- .ohm-body { padding: 24px 26px 26px; overflow-y: scroll; }
4260
- .ohm-body::-webkit-scrollbar { width: 18px; }
4261
- .ohm-body::-webkit-scrollbar-track { background: transparent; margin: 12px 0; }
4262
- .ohm-body::-webkit-scrollbar-thumb { background-color: #111; border: 6px solid transparent; background-clip: padding-box; border-radius: 999px; min-height: 44px; }
4263
- .ohm-body::-webkit-scrollbar-thumb:hover { background-color: #000; }
4264
- .ohm-logo { display: block; margin: 6px auto 18px; max-width: 64%; height: auto; }
4265
- .ohm-logo-text { margin: 6px 0 18px; text-align: center; font-size: 30px; font-weight: 900; letter-spacing: 1px; color: var(--text); }
4266
- .ohm-sec { display: flex; align-items: center; gap: 14px; margin: 26px 0 14px; color: var(--text); font-weight: 800; letter-spacing: 1px; }
4267
- .ohm-sec::before, .ohm-sec::after { content: ""; flex: 1; height: 2px; background: color-mix(in srgb, var(--text) 80%, transparent); border-radius: 2px; }
4268
- .ohm-root *, .ohm-root *::before, .ohm-root *::after { box-sizing: border-box; }
4269
- .ohm-row { display: flex; align-items: center; gap: 16px; margin: 14px 0; font-weight: 700; }
4270
- .ohm-setting { margin: 14px 0; }
4271
- .ohm-setting .ohm-row { margin: 0; }
4272
- .ohm-hint { margin: 3px 0 0; font-size: 12.5px; font-weight: 500; color: var(--text-dim); }
4273
- .ohm-row > span:first-child { flex: none; min-width: 110px; }
4274
- .ohm-row input[type=range] { flex: 1; min-width: 0; max-width: min(440px, 60dvw); accent-color: var(--accent); height: 6px; }
4275
- .ohm-row select { flex: 1; min-width: 0; max-width: min(440px, 60dvw); padding: 11px 14px; border-radius: 4px; border: 2px solid var(--accent); background: var(--surface-alt); color: var(--text); font-weight: 700; font-size: 15px; cursor: pointer; }
4276
- .ohm-row select option { background: var(--surface); color: var(--text); font-weight: 600; }
4277
- .ohm-row select option:checked { background: var(--accent); color: var(--accent-text); }
4278
- .ohm-ctl { flex: 1; min-width: 0; max-width: min(440px, 60dvw); display: flex; align-items: center; justify-content: flex-end; }
4279
- .ohm-check input[type=checkbox] { appearance: none; -webkit-appearance: none; width: 50px; height: 28px; border-radius: 999px; background: color-mix(in srgb, var(--text-dim) 38%, transparent); position: relative; cursor: pointer; transition: background .15s; flex: none; }
4280
- .ohm-check input[type=checkbox]:checked { background: var(--accent); }
4281
- .ohm-check input[type=checkbox]::before { content: ""; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 999px; background: #fff; transition: left .15s; box-shadow: 0 1px 3px rgba(0,0,0,.3); }
4282
- .ohm-check input[type=checkbox]:checked::before { left: 25px; }
4283
- .ohm-segmented { display: inline-flex; gap: 4px; padding: 4px; background: var(--surface-alt); border-radius: 999px; border: 1px solid color-mix(in srgb, var(--text-dim) 30%, transparent); }
4284
- .ohm-seg { border: 0; background: transparent; color: var(--text-dim); font-weight: 700; font-size: 14px; padding: 8px 18px; border-radius: 999px; cursor: pointer; transition: background .12s, color .12s; }
4285
- .ohm-seg.active { background: var(--accent); color: var(--accent-text); }
4286
- .ohm-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
4287
- .ohm-sym { display: flex; align-items: center; gap: 16px; padding: 6px 4px; }
4288
- .ohm-emoji { font-size: 48px; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,.3)); }
4289
- .ohm-symimg { width: 56px; height: 56px; object-fit: contain; flex: none; }
4290
- .ohm-pay { font-size: 13px; line-height: 1.6; }
4291
- .ohm-pay div { display: flex; gap: 6px; }
4292
- .ohm-pay b { min-width: 42px; }
4293
- .ohm-pay span { color: var(--accent); font-weight: 700; }
4294
- .ohm-body p { color: var(--text-dim); line-height: 1.6; }
4295
- .ohm-body p b { color: var(--text); }
4296
- .ohm-feature { display: block; width: 100%; height: auto; margin: 10px 0; }
4297
- .ohm-stats { margin: 12px 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; }
4298
- .ohm-stats > div { display: flex; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 20%, transparent); }
4299
- .ohm-stats dt { color: var(--text-dim); margin: 0; } .ohm-stats dd { margin: 0; font-weight: 700; }
4300
- .ohm-audit { margin: 12px 0 18px; padding: 14px 16px; border-radius: 12px; border: 2px solid #d03131; background: color-mix(in srgb, #d03131 8%, transparent); }
4301
- .ohm-audit-title { font-weight: 900; letter-spacing: .5px; color: #b31d1d; }
4302
- .ohm-audit-sub { margin-top: 8px; font-size: 13px; font-weight: 800; color: #b31d1d; }
4303
- .ohm-audit-sub--rec { color: #b07d09; }
4304
- .ohm-audit ul { margin: 6px 0 0; padding-left: 20px; color: var(--text); font-size: 13.5px; line-height: 1.55; }
4305
- .ohm-audit li { margin: 3px 0; }
4306
- .ohm-audit-rec ul { color: var(--text-dim); }
4307
- .ohm-callout { margin: 16px 0 4px; padding: 14px 16px; border-radius: 12px; border-left: 4px solid var(--accent); background: color-mix(in srgb, var(--accent) 9%, transparent); }
4308
- .ohm-callout b { color: var(--accent); } .ohm-callout p { margin: 4px 0 0; color: var(--text); }
4309
- .ohm-callout--warning { border-left-color: #e0a106; background: color-mix(in srgb, #e0a106 10%, transparent); }
4310
- .ohm-callout--warning b { color: #b07d09; }
4311
- .ohm-subh { margin: 22px 0 8px; font-size: 15px; font-weight: 800; letter-spacing: .5px; color: var(--text); }
4312
- .ohm-legal { font-size: 12px; line-height: 1.6; color: var(--text-dim); opacity: .85; }
4313
- .ohm-hr { border: 0; border-top: 1px solid color-mix(in srgb, var(--text-dim) 30%, transparent); margin: 18px 0; }
4314
- .ohm-media { display: flex; align-items: center; gap: 18px; margin: 14px 0; }
4315
- .ohm-media--right { flex-direction: row-reverse; }
4316
- .ohm-media > img { width: 40%; max-width: 320px; height: auto; flex: none; }
4317
- .ohm-media-body { flex: 1; }
4318
- .ohm-media-body h4 { margin: 0 0 6px; font-size: 16px; color: var(--text); }
4319
- .ohm-media-body p { margin: 0; }
4320
- .ohm-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 12px 0; }
4321
- .ohm-fcard { padding: 8px 6px; text-align: center; }
4322
- .ohm-fcard img { display: block; width: 48px; height: 48px; margin: 0 auto 8px; }
4323
- .ohm-fcard h5 { margin: 0 0 4px; font-size: 14px; color: var(--text); }
4324
- .ohm-fcard p { margin: 0; font-size: 12px; line-height: 1.5; }
4325
- .ohm-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; }
4326
- .ohm-table th { text-align: left; padding: 8px 10px; font-weight: 800; color: var(--text); border-bottom: 2px solid color-mix(in srgb, var(--text-dim) 35%, transparent); }
4327
- .ohm-table td { padding: 8px 10px; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 18%, transparent); }
4328
- .ohm-table td:first-child { color: var(--text); font-weight: 700; }
4329
- .ohm-table td:not(:first-child) { color: var(--accent); font-weight: 700; }
4330
- .ohm-steps { margin: 12px 0; padding-left: 22px; color: var(--text-dim); line-height: 1.7; }
4331
- .ohm-steps li { margin: 5px 0; }
4332
- .ohm-steps b { color: var(--text); }
4333
- .ohm-group { margin: 8px 0; }
4334
- `;
4335
4147
 
4336
4148
  // src/confirmModal.ts
4337
4149
  var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -4587,7 +4399,7 @@ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
4587
4399
  const layout = () => {
4588
4400
  const vw = window.innerWidth;
4589
4401
  const vh = window.innerHeight;
4590
- const cols = vh <= 540 || vw >= 900 ? Math.min(4, list.length) : Math.min(2, list.length);
4402
+ const cols = vw >= 720 && (vw >= 900 || vh <= 540) ? Math.min(4, list.length) : Math.min(2, list.length);
4591
4403
  cardsEl.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
4592
4404
  const fitW = Math.min(vw * 0.96, cols >= 4 ? 1180 : cols === 1 ? 380 : 760);
4593
4405
  fitEl.style.width = `${fitW}px`;
@@ -4604,7 +4416,8 @@ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
4604
4416
  cardsEl.innerHTML = list.map((f) => {
4605
4417
  const active = boosts.has(f.id);
4606
4418
  const buyBlocked = f.variant === "buy" && blocksBuy && boosts.size > 0;
4607
- const price = f.variant === "buy" ? money(f.cost * bet, cur) : `+${money(f.cost * bet, cur)}`;
4419
+ const perSpin = (f.variant === "buy" ? f.cost : 1 + f.cost) * bet;
4420
+ const price = money(perSpin, cur);
4608
4421
  const label = f.variant === "buy" ? tr("Buy") : active ? tr("Activated") : tr("Activate");
4609
4422
  const cls = `bfm-action bfm-action--${f.variant}${active ? " is-active" : ""}${buyBlocked ? " is-blocked" : ""}`;
4610
4423
  const img = f.image ? ` style="background-image:url('${f.image}')"` : "";