@bongos/core 1.19.386 → 1.19.388
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/.bongos-core.json +138 -53
- package/config/branding.neutral.json +19 -19
- package/config/design-tokens.neutral.json +31 -36
- package/docs/adr/0196-chrome-world-token-contract-and-the-landing-sheets.md +31 -0
- package/docs/adr/README.md +1 -0
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +156 -133
- package/docs/copy-registry.json +537 -286
- package/docs/file-map.md +14 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/renderer/index.html +1 -1
- package/modules/dev-box/app/renderer/styles.css +3 -1
- package/modules/dev-box/app/renderer/theme.generated.css +13 -13
- package/modules/dev-box/app/scripts/gen-brand.js +13 -13
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +3 -3
- package/modules/dev-box/app/scripts/gen-icons.js +3 -3
- package/modules/dev-box/app/src/brand.generated.json +1 -1
- package/modules/hall-ui/public/government.css +70 -0
- package/modules/hall-ui/public/government.html +9 -2
- package/modules/hall-ui/public/government.js +175 -32
- package/modules/provisioning/routes/provisioning.js +4 -4
- package/modules/status-ui/public/explore.html +12 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +82 -64
- package/public-landing/assets/world/og-card.jpg +0 -0
- package/public-landing/assets/world/r2-bongos-chrome-a-cut.webp +0 -0
- package/public-landing/assets/world/r2-bongos-chrome-a-cut.webp.json +12 -0
- package/public-landing/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/public-landing/assets/world/r2-bongos-chrome-b.jpg.json +12 -0
- package/public-landing/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/public-landing/assets/world/r2-chrome-blob-one-cut.webp.json +13 -0
- package/public-landing/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/public-landing/assets/world/r2-chrome-blob-one.jpg.json +12 -0
- package/public-landing/assets/world/r2-chrome-blobs-set-cut.webp +0 -0
- package/public-landing/assets/world/r2-chrome-blobs-set-cut.webp.json +13 -0
- package/public-landing/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/public-landing/assets/world/r2-chrome-blobs-set.jpg.json +12 -0
- package/public-landing/assets/world.css +241 -0
- package/public-landing/contact.html +1 -1
- package/public-landing/index.html +1139 -580
- package/public-landing/privacy.html +1 -1
- package/public-landing/projects.html +2 -2
- package/public-landing/terms.html +1 -1
- package/src/bongos/platform-visibility-gate.js +8 -7
- package/src/bongos/stealth-gate.js +9 -8
- package/src/module-api.js +5 -1
- package/src/ui/adapters/claude-design/index.js +2 -2
- package/tests/cloud_bongos_pack.mjs +22 -18
- package/tests/cosmos_token_contrast.mjs +23 -8
- package/tests/government_board_room_copy.mjs +131 -0
- package/tests/landing_legal_pages.mjs +8 -5
- package/tests/landing_page.mjs +295 -166
- package/tests/platform_boot.mjs +3 -1
- package/tests/projects_hub.mjs +9 -5
- package/tests/projects_hub_pre_uat.mjs +3 -1
- package/tests/world_tokens.mjs +351 -0
package/docs/file-map.md
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
/
|
|
10
10
|
├── CLAUDE.md ← index + framing (descriptive; DB is source of truth for state)
|
|
11
11
|
├── README.md ← public-facing one-paragraph summary
|
|
12
|
+
├── PRODUCT.md ← durable product truth for the apex/hall/status surfaces (impeccable init, task 1003296): users, positioning, constraints, brand commitments, principles — no palette, no type
|
|
13
|
+
├── DESIGN.md ← the visual rulebook written FROM the built landing (task 1003298, ADR 0196): palette + derived recipes, type, materials (the chrome plates), components, the motion grammar, both modes; what H1 (hall) and S1 (status) build against. Sidecar .impeccable/design.json
|
|
14
|
+
├── .impeccable/ ← impeccable skill artifacts: config.json (buildPath: code), surfaces/<route>.md (per-route briefs), mocks/world-assets/ (Higgsfield renders + prompt sidecars; the SOURCE of public-landing/assets/world/), mocks/decision/ (A1 comps), design.json (DESIGN.md sidecar), review/ (finish captures)
|
|
12
15
|
├── .gitignore
|
|
13
16
|
├── package.json / package-lock.json
|
|
14
17
|
├── CODEOWNERS ← owner review on the merge-gate surfaces (real since task 1002611, ADR 0159); inert until branch protection is on (scripts/gds/gating.js) — keep aligned with gate-review.js HARD_FLOOR_GLOBS
|
|
@@ -246,6 +249,17 @@
|
|
|
246
249
|
│ ├── assets/tray/ + build-resources/ ← generated icons (scripts/gen-icons.js; build-resources name dodges the global build/ gitignore)
|
|
247
250
|
│ └── test/unit.mjs ← hermetic unit tests (run by the CI workflow, not the root gate)
|
|
248
251
|
│
|
|
252
|
+
├── public-landing/ ← the APEX front door + hub surfaces served by src/platform-server.js (landingPage(): apex-only strip → brand-token stamp → theme injection → ?v= asset stamp). Ships to EVERY instance; <!--apex-only:start/end--> regions are removed on a child
|
|
253
|
+
│ ├── index.html ← the landing (/) — v3 "the template's page" of the chrome world (task 1003298, ADR 0196): server-stamped head, the breath (CSS-only signature), live figures/ledger/orbs from the public feeds baked inside sentinels; page-local <style> only (no :root), links /assets/world.css
|
|
254
|
+
│ ├── projects.html ← the hub (/projects?view=map|mine|manage|new) on cosmos.css (A4, task 1003299, rebuilds it in the world)
|
|
255
|
+
│ ├── privacy.html / terms.html / contact.html ← the footer doors (task 1003251), on cosmos.css
|
|
256
|
+
│ ├── assets/
|
|
257
|
+
│ │ ├── world.css ← the WORLD token layer (ADR 0196): contract tier = the pack, derived tier both modes, root-absolute plate token pairs, the reduced-motion kill switch; no page composition
|
|
258
|
+
│ │ ├── world/ ← the six production plates + <file>.json provenance sidecars (embed-prompt.mjs --scan clean): r2-chrome-blobs-set.jpg, r2-chrome-blob-one.jpg, r2-bongos-chrome-b.jpg (black plates, dark mode, lighten) and their -cut.webp alpha twins (light mode); og-card.jpg (the social card, captured from the page)
|
|
259
|
+
│ │ ├── cosmos.css ← the HUB component sheet (task 1002696): contract tier synced to the pack, its own derived + dark tiers, the .btn/.chip/.strip/.proj/.tbl vocabulary; measured by tests/cosmos_token_contrast.mjs
|
|
260
|
+
│ │ └── verro-medallion.png ← the Verro Labs mark (apex-only block)
|
|
261
|
+
│ └── world/assets/ ← the RETIRED gate's clouds/creatures/plates (task 1002407; the gate folded into / in 1003233) — not the chrome plates
|
|
262
|
+
│
|
|
249
263
|
├── public/ ← example.com — the game (product)
|
|
250
264
|
│ ├── index.html
|
|
251
265
|
│ ├── assets/
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<title>Dev Box</title>
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
11
11
|
<link rel="stylesheet" href="theme.generated.css">
|
|
12
12
|
<link rel="stylesheet" href="styles.css">
|
|
13
13
|
</head>
|
|
@@ -154,7 +154,9 @@ main {
|
|
|
154
154
|
width: 100%;
|
|
155
155
|
margin-top: 10px;
|
|
156
156
|
}
|
|
157
|
-
|
|
157
|
+
/* the hover fill is the raw accent; the chrome world's accent carries a light ink
|
|
158
|
+
at 2.19:1, so the label flips to --ink on it (8.12:1) — task 1003298 */
|
|
159
|
+
.btn:hover:not(:disabled) { background: var(--accent); border-color: var(--accent); color: var(--ink); }
|
|
158
160
|
.btn:disabled { opacity: 0.55; cursor: default; }
|
|
159
161
|
.btn--ghost {
|
|
160
162
|
background: transparent;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/* GENERATED by scripts/gen-brand.js from config/branding.json — do not edit by hand. */
|
|
2
2
|
:root {
|
|
3
|
-
--bg: #
|
|
4
|
-
--bg-card: #
|
|
5
|
-
--bg-deep: #
|
|
6
|
-
--ink: #
|
|
7
|
-
--ink-soft: #
|
|
8
|
-
--ink-faint: #
|
|
9
|
-
--rule: #
|
|
10
|
-
--rule-soft: #
|
|
11
|
-
--accent: #
|
|
12
|
-
--accent-deep: #
|
|
13
|
-
--terracotta: #
|
|
14
|
-
--font-display: "
|
|
15
|
-
--font-body: "
|
|
3
|
+
--bg: #edecee;
|
|
4
|
+
--bg-card: #fbfafc;
|
|
5
|
+
--bg-deep: #e2e1e4;
|
|
6
|
+
--ink: #17181a;
|
|
7
|
+
--ink-soft: #55565a;
|
|
8
|
+
--ink-faint: #65656a;
|
|
9
|
+
--rule: #c6c6c8;
|
|
10
|
+
--rule-soft: #dcdbdd;
|
|
11
|
+
--accent: #fb9569;
|
|
12
|
+
--accent-deep: #a3481a;
|
|
13
|
+
--terracotta: #c9512c;
|
|
14
|
+
--font-display: "Manrope", "Segoe UI", system-ui, -apple-system, sans-serif;
|
|
15
|
+
--font-body: "Manrope", "Segoe UI", system-ui, -apple-system, sans-serif;
|
|
16
16
|
}
|
|
@@ -73,7 +73,7 @@ function buildBrand(branding) {
|
|
|
73
73
|
// Panel header epigraph (branding copy.tagline).
|
|
74
74
|
TAGLINE: (branding.copy && branding.copy.tagline) || '',
|
|
75
75
|
// Pre-CSS window background (main.js) — the theme bg so first paint is on-brand.
|
|
76
|
-
BG_COLOR: colors.bg || '#
|
|
76
|
+
BG_COLOR: colors.bg || '#fbfafc',
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -85,19 +85,19 @@ function buildThemeCss(branding) {
|
|
|
85
85
|
const v = (x, fb) => x || fb;
|
|
86
86
|
return `/* GENERATED by scripts/gen-brand.js from config/branding.json — do not edit by hand. */
|
|
87
87
|
:root {
|
|
88
|
-
--bg: ${v(c.bg, '#
|
|
89
|
-
--bg-card: ${v(c.bgCard, '#
|
|
90
|
-
--bg-deep: ${v(c.bgDeep, '#
|
|
91
|
-
--ink: ${v(c.ink, '#
|
|
92
|
-
--ink-soft: ${v(c.inkSoft, '#
|
|
88
|
+
--bg: ${v(c.bg, '#edecee')};
|
|
89
|
+
--bg-card: ${v(c.bgCard, '#fbfafc')};
|
|
90
|
+
--bg-deep: ${v(c.bgDeep, '#e2e1e4')};
|
|
91
|
+
--ink: ${v(c.ink, '#17181a')};
|
|
92
|
+
--ink-soft: ${v(c.inkSoft, '#55565a')};
|
|
93
93
|
--ink-faint: ${v(c.inkFaint, '#785a41')};
|
|
94
|
-
--rule: ${v(c.rule, '#
|
|
95
|
-
--rule-soft: ${v(c.ruleSoft, '#
|
|
96
|
-
--accent: ${v(c.accent, '#
|
|
97
|
-
--accent-deep: ${v(c.accentDeep, '#
|
|
98
|
-
--terracotta: ${v(c.terracotta, '#
|
|
99
|
-
--font-display: ${v(fonts.display, '"
|
|
100
|
-
--font-body: ${v(fonts.body, '"
|
|
94
|
+
--rule: ${v(c.rule, '#c6c6c8')};
|
|
95
|
+
--rule-soft: ${v(c.ruleSoft, '#dcdbdd')};
|
|
96
|
+
--accent: ${v(c.accent, '#fb9569')};
|
|
97
|
+
--accent-deep: ${v(c.accentDeep, '#a3481a')};
|
|
98
|
+
--terracotta: ${v(c.terracotta, '#c9512c')};
|
|
99
|
+
--font-display: ${v(fonts.display, '"Manrope", "Segoe UI", system-ui, sans-serif')};
|
|
100
|
+
--font-body: ${v(fonts.body, '"Manrope", "Segoe UI", system-ui, -apple-system, sans-serif')};
|
|
101
101
|
}
|
|
102
102
|
`;
|
|
103
103
|
}
|
|
@@ -68,9 +68,9 @@ ICON = 100 # dmg.iconSize
|
|
|
68
68
|
# it, so its "fallback" would paint a Cloud Bongos colour into a fully-specified
|
|
69
69
|
# host build. That is exactly what `sea` did to the Applications-well rim.
|
|
70
70
|
COLOR_ROLES = {
|
|
71
|
-
"bg": "#
|
|
72
|
-
"ink": "#
|
|
73
|
-
"accent": "#
|
|
71
|
+
"bg": "#edecee", "bgDeep": "#e2e1e4", "bgCard": "#fbfafc",
|
|
72
|
+
"ink": "#17181a", "inkSoft": "#55565a", "ruleSoft": "#dcdbdd",
|
|
73
|
+
"accent": "#fb9569", "accentDeep": "#a3481a", "terracotta": "#c9512c",
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
FONT_DIRS = (
|
|
@@ -62,9 +62,9 @@ const OUT_BUILD = path.join(APP_DIR, 'build-resources');
|
|
|
62
62
|
// it has nothing prompting a host to define it, so its "fallback" would paint a
|
|
63
63
|
// Cloud Bongos colour into a fully-specified host build.
|
|
64
64
|
const COLOR_ROLES = {
|
|
65
|
-
bgCard: '#
|
|
66
|
-
rule: '#
|
|
67
|
-
accent: '#
|
|
65
|
+
bgCard: '#fbfafc', bgDeep: '#e2e1e4', ink: '#17181a',
|
|
66
|
+
rule: '#c6c6c8', ruleSoft: '#dcdbdd',
|
|
67
|
+
accent: '#fb9569', accentDeep: '#a3481a', terracotta: '#c9512c',
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
// The pack, resolved exactly as gen-brand.js resolves it (env → instance → neutral).
|
|
@@ -101,3 +101,73 @@
|
|
|
101
101
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
|
|
102
102
|
color: var(--accent-text); background: var(--surface-2); padding: 1px 5px; border-radius: 4px;
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/* ---- the Board Room: one sitting, one decision (task 1003270) -------------
|
|
106
|
+
The item card is NOT `.gov-section`. It used to be, which meant the title of
|
|
107
|
+
the idea being voted on rendered through `.gov-section__head` — 11.5px,
|
|
108
|
+
uppercase, letterspaced, i.e. styled as a table header. Here the title is a
|
|
109
|
+
title, the ballot sits directly under it, the idea's own words are body text,
|
|
110
|
+
and the constitutional metadata is the only thing behind a click. */
|
|
111
|
+
.gov-item {
|
|
112
|
+
margin: 0 0 18px; border: 1px solid var(--border); border-radius: var(--radius);
|
|
113
|
+
background: var(--surface); overflow: hidden;
|
|
114
|
+
}
|
|
115
|
+
.gov-item__head {
|
|
116
|
+
display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
|
|
117
|
+
gap: 6px 12px; padding: 12px 14px 10px;
|
|
118
|
+
}
|
|
119
|
+
.gov-item__title {
|
|
120
|
+
margin: 0; min-width: 0; font-size: 16px; font-weight: 600; line-height: 1.35;
|
|
121
|
+
color: var(--ink); overflow-wrap: anywhere;
|
|
122
|
+
}
|
|
123
|
+
.gov-item__title a { color: inherit; }
|
|
124
|
+
.gov-item__state { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; }
|
|
125
|
+
.gov-item__amend { margin: 0; padding: 0 14px 10px; color: var(--ink-soft); font-size: 12.5px; overflow-wrap: anywhere; }
|
|
126
|
+
|
|
127
|
+
/* The primary action, directly under the title — that adjacency is the fix. */
|
|
128
|
+
.gov-item__act { padding: 2px 14px 14px; }
|
|
129
|
+
.gov-item__cast { margin: 0; color: var(--ink-soft); font-size: 12.5px; }
|
|
130
|
+
|
|
131
|
+
/* The idea itself: open, and set as prose rather than as permission rows. */
|
|
132
|
+
.gov-idea { padding: 12px 14px 2px; border-top: 1px solid var(--border); }
|
|
133
|
+
.gov-idea__part { margin: 0 0 12px; }
|
|
134
|
+
.gov-idea__label { margin: 0 0 3px; font-size: 12px; font-weight: 600; color: var(--ink-soft); }
|
|
135
|
+
.gov-idea__body {
|
|
136
|
+
margin: 0; color: var(--ink); font-size: 13.5px; line-height: 1.5;
|
|
137
|
+
white-space: pre-wrap; overflow-wrap: anywhere;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Secondary: the tally rides the summary (visible without a click — ADR 0175
|
|
141
|
+
§9), the rules and the open ballot sit inside it. */
|
|
142
|
+
.gov-item__meta { border-top: 1px solid var(--border); }
|
|
143
|
+
.gov-item__summary {
|
|
144
|
+
padding: 8px 14px; cursor: pointer; color: var(--ink-soft); font-size: 12px;
|
|
145
|
+
list-style-position: inside;
|
|
146
|
+
}
|
|
147
|
+
.gov-item__summary:hover { color: var(--ink); }
|
|
148
|
+
.gov-item__metabody { padding: 10px 14px 2px; background: var(--surface-2); }
|
|
149
|
+
.gov-item__metabody .gov-section { margin: 0 0 12px; background: var(--surface); }
|
|
150
|
+
.gov-item__metabody .gov-empty { padding: 0 0 12px; }
|
|
151
|
+
|
|
152
|
+
/* ---- the ballot controls ------------------------------------------------- */
|
|
153
|
+
.gov-vote { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
|
|
154
|
+
.gov-vote > .gov-grid-note { margin: 0; max-width: 62ch; }
|
|
155
|
+
.gov-vote__no { align-self: stretch; }
|
|
156
|
+
/* The disclosure IS the secondary button, so its marker must go — otherwise the
|
|
157
|
+
triangle sits inside the pill. */
|
|
158
|
+
.gov-vote__no > summary { display: inline-block; cursor: pointer; }
|
|
159
|
+
.gov-vote__no > summary::marker { content: ''; }
|
|
160
|
+
.gov-vote__no > summary::-webkit-details-marker { display: none; }
|
|
161
|
+
.gov-vote__no > *:not(summary) { margin-top: 8px; }
|
|
162
|
+
.gov-vote__no > textarea.gov-input { display: block; width: 100%; max-width: 62ch; }
|
|
163
|
+
.gov-vote__sections { display: flex; flex-wrap: wrap; gap: 0 18px; }
|
|
164
|
+
.gov-vote__sections .gov-perm {
|
|
165
|
+
padding: 2px 0; border-bottom: 0; cursor: pointer; font-size: 12.5px; color: var(--ink);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* The ballot rows reuse `.gov-assign-row`, whose four flex children (name, pill,
|
|
169
|
+
timestamp, objection) cannot fit one 390w line — the objection is a paragraph,
|
|
170
|
+
not a cell, so it takes its own row and the rest wraps. Scoped to the metabody
|
|
171
|
+
so the Permissions and Constitution tabs keep their single-line rows. */
|
|
172
|
+
.gov-item__metabody .gov-assign-row { flex-wrap: wrap; }
|
|
173
|
+
.gov-item__metabody .gov-assign-row .gov-perm__guards { flex: 1 1 100%; margin-top: 4px; }
|
|
@@ -37,14 +37,21 @@
|
|
|
37
37
|
R18/R19; until then the section frames the model honestly. -->
|
|
38
38
|
<section id="gov-panel-constitution" hidden>
|
|
39
39
|
<h2 class="gov-h">Constitution</h2>
|
|
40
|
-
|
|
40
|
+
<!-- Task 1003270: the ~60-word explainer that used to sit here said,
|
|
41
|
+
at length, what the three live sentences below say from the real
|
|
42
|
+
config. A reader met the paragraph before the values. -->
|
|
43
|
+
<p class="gov-grid-note">Three dials, all settings rather than code: <strong>who sits on the board</strong>, <strong>what counts as passing</strong>, and <strong>how long a vote stays open</strong>.</p>
|
|
41
44
|
<div id="gov-constitution"></div>
|
|
42
45
|
</section>
|
|
43
46
|
|
|
44
47
|
<!-- §2 Board Room — open items, the clock, your vote (R16/R17). -->
|
|
45
48
|
<section id="gov-panel-board" hidden>
|
|
46
49
|
<h2 class="gov-h">Board Room</h2>
|
|
47
|
-
|
|
50
|
+
<!-- Task 1003270: one line, then the ballot. Everything the old
|
|
51
|
+
~50-word paragraph explained is now said by the cards themselves
|
|
52
|
+
— each one carries who votes and how it passes, in plain words,
|
|
53
|
+
under its own summary. -->
|
|
54
|
+
<p class="gov-grid-note">Read an idea, then decide. An objection needs a reason and the section it faults.</p>
|
|
48
55
|
<div id="gov-board"></div>
|
|
49
56
|
</section>
|
|
50
57
|
|
|
@@ -42,6 +42,22 @@
|
|
|
42
42
|
// items and the vote. Until a frame is filled it states what belongs there
|
|
43
43
|
// and shows an honest kit empty state — never a fake or a hardcoded value that
|
|
44
44
|
// could drift from the real config.
|
|
45
|
+
//
|
|
46
|
+
// TASK 1003270 rebuilt the BOARD ROOM CARD around one claim: the idea is the
|
|
47
|
+
// subject, everything else is apparatus. So the idea's title renders at title
|
|
48
|
+
// size and its five sections render open as prose; the ballot sits directly
|
|
49
|
+
// beneath the title; and the government apparatus — who votes, how it passes,
|
|
50
|
+
// the clock, the open ballot — moved behind one summary that still shows the
|
|
51
|
+
// tally. Two rules came out of it and should survive the next edit here:
|
|
52
|
+
// • NO RAW CONFIG KEY REACHES A READER. `first_ratifier` / `rank:archon` /
|
|
53
|
+
// `why_matters` are grammar for the server; a person gets the sentence
|
|
54
|
+
// (decidedByHtml, rankNames, SECTION_LABELS). The single exception is a
|
|
55
|
+
// membership predicate that cannot be PARSED, where the broken string is
|
|
56
|
+
// the actionable fact.
|
|
57
|
+
// • COPY THAT EXPLAINS THE UI TO ITSELF IS DELETED, not shortened.
|
|
58
|
+
// What simplifying may NOT touch is ADR 0175 §8: an objection still carries a
|
|
59
|
+
// reason and names one section. That requirement is why voting lives in the
|
|
60
|
+
// hall and not on a chat reaction.
|
|
45
61
|
|
|
46
62
|
(() => {
|
|
47
63
|
'use strict';
|
|
@@ -497,7 +513,12 @@
|
|
|
497
513
|
// and fixable, a board described as working is neither.
|
|
498
514
|
return `Membership predicate <code>${escapeHtml(raw || 'unset')}</code> cannot be read, so <strong>nobody</strong> sits on the board until it is corrected.`;
|
|
499
515
|
}
|
|
500
|
-
|
|
516
|
+
// Task 1003270: the DISPLAY label, not the Greek enum key. The enum is the
|
|
517
|
+
// grammar an owner types into the predicate (and the charter library below
|
|
518
|
+
// still shows it verbatim, which is that library's whole job); this sentence
|
|
519
|
+
// is the one that answers "who governs", so it uses the words the rest of
|
|
520
|
+
// the hall shows a rank in.
|
|
521
|
+
const strong = keys.map((k) => `<strong>${escapeHtml(window.OTB.rankLabel(k))}</strong>`);
|
|
501
522
|
if (keys.length === 1) {
|
|
502
523
|
return `Every active builder holding the ${strong[0]} rank — and only that rank — sits on the board. Membership follows the rank, live.`;
|
|
503
524
|
}
|
|
@@ -536,7 +557,7 @@
|
|
|
536
557
|
const early = board.close_early_on_full_turnout
|
|
537
558
|
? ', closing early the moment every member has voted'
|
|
538
559
|
: '';
|
|
539
|
-
return `A sitting runs <strong>${
|
|
560
|
+
return `A sitting runs <strong>${fmtWindow(board.window_minutes)}</strong>${early}. An unvoted window nulls that member's vote — absence never blocks.`;
|
|
540
561
|
}
|
|
541
562
|
|
|
542
563
|
function historyRow(h) {
|
|
@@ -545,8 +566,10 @@
|
|
|
545
566
|
const pill = h.outcome === 'passed'
|
|
546
567
|
? '<span class="gov-pill gov-pill--seeded">ratified</span>'
|
|
547
568
|
: '<span class="gov-pill gov-pill--locked">returned</span>';
|
|
569
|
+
// Task 1003270: the rule by name, not by config key. An unknown value still
|
|
570
|
+
// shows verbatim — a rule this build does not know about is worth seeing.
|
|
548
571
|
const under = h.decided_under && h.decided_under.pass_rule
|
|
549
|
-
? ` · decided
|
|
572
|
+
? ` · decided ${escapeHtml(PASS_RULE_NAME[h.decided_under.pass_rule] || `under ${h.decided_under.pass_rule}`)}`
|
|
550
573
|
: '';
|
|
551
574
|
return `<div class="gov-assign-row">
|
|
552
575
|
<span class="gov-assign-row__who">${who} proposed <code>${escapeHtml(JSON.stringify(h.proposed))}</code>${h.rationale_md ? ` — ${escapeHtml(h.rationale_md)}` : ''}</span>
|
|
@@ -717,11 +740,14 @@
|
|
|
717
740
|
const who = escapeHtml(v.display_name || v.github_login || `builder ${v.voter_id}`);
|
|
718
741
|
const mine = String(v.voter_id) === String(viewerId) ? ' <span class="gov-held-note">your vote</span>' : '';
|
|
719
742
|
const when = escapeHtml(String(v.cast_at).slice(0, 16).replace('T', ' '));
|
|
743
|
+
// Task 1003270: the faulted section by its LABEL — the words the author
|
|
744
|
+
// wrote under and the objector picked from. `why_matters` is a jsonb key,
|
|
745
|
+
// not something a reader should have to decode.
|
|
720
746
|
const objection = v.direction === 'no'
|
|
721
|
-
? `<div class="gov-perm__guards">objected to <
|
|
747
|
+
? `<div class="gov-perm__guards">objected to <strong>${escapeHtml(SECTION_LABELS[v.section_key] || v.section_key || 'the idea')}</strong> — ${escapeHtml(v.reason_md || '')}</div>`
|
|
722
748
|
: '';
|
|
723
749
|
return `<div class="gov-assign-row"><span class="gov-assign-row__who">${who}${mine}</span>
|
|
724
|
-
<span class="gov-pill ${v.direction === 'no' ? 'gov-pill--locked' : 'gov-pill--seeded'}">${v.direction}</span>
|
|
750
|
+
<span class="gov-pill ${v.direction === 'no' ? 'gov-pill--locked' : 'gov-pill--seeded'}">${v.direction === 'no' ? 'objected' : 'yes'}</span>
|
|
725
751
|
<span class="muted">${when}</span>${objection}</div>`;
|
|
726
752
|
}).join('');
|
|
727
753
|
return `<div class="gov-section">${rows}</div>`;
|
|
@@ -742,20 +768,102 @@
|
|
|
742
768
|
|
|
743
769
|
// The idea's five sections, IN FRONT of the voter — the entire reason voting
|
|
744
770
|
// happens in the hall rather than on a reaction. Rendered in template order.
|
|
771
|
+
//
|
|
772
|
+
// TASK 1003270 — OPEN, AND SET AS PROSE. This used to be a <details> summarised
|
|
773
|
+
// "The idea, in full · completeness 100/100": the substance the vote is about
|
|
774
|
+
// sat behind a click while the constitutional metadata above it stayed expanded,
|
|
775
|
+
// which is exactly backwards. The labels were <code> pills over
|
|
776
|
+
// `.gov-perm__guards`, so an author's answers were styled as config rows. The
|
|
777
|
+
// score moved into the collapsed metadata (decidedByHtml) — it describes the
|
|
778
|
+
// grade the idea cleared, not the idea.
|
|
745
779
|
function ideaSectionsHtml(item) {
|
|
746
780
|
const fv = item.subject && item.subject.field_values;
|
|
747
781
|
if (!fv) return '';
|
|
748
|
-
const rows = Object.keys(SECTION_LABELS).map((key) =>
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
782
|
+
const rows = Object.keys(SECTION_LABELS).map((key) => {
|
|
783
|
+
// An unanswered section is dropped rather than rendered as a bare heading:
|
|
784
|
+
// a graded Full Idea has all five, and a blank one is only noise.
|
|
785
|
+
const body = String(fv[key] ?? '').trim();
|
|
786
|
+
if (!body) return '';
|
|
787
|
+
return `<div class="gov-idea__part">
|
|
788
|
+
<h4 class="gov-idea__label">${escapeHtml(SECTION_LABELS[key])}</h4>
|
|
789
|
+
<p class="gov-idea__body">${escapeHtml(body)}</p>
|
|
790
|
+
</div>`;
|
|
791
|
+
}).join('');
|
|
792
|
+
return rows
|
|
793
|
+
? `<div class="gov-idea">${rows}</div>`
|
|
794
|
+
: '<p class="gov-empty">This idea has no written sections.</p>';
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ---- the sitting's rules, in plain words (task 1003270) --------------------
|
|
798
|
+
|
|
799
|
+
// The card used to carry `tally: 0 yes · 0 no — sitting under <first_ratifier>,
|
|
800
|
+
// membership <rank:archon>` — raw config keys, in a reader's face, ABOVE the
|
|
801
|
+
// idea they were being asked to decide. These say the same thing in the words
|
|
802
|
+
// a person thinks in, filed under a summary where secondary information
|
|
803
|
+
// belongs.
|
|
804
|
+
//
|
|
805
|
+
// Read from the item's SNAPSHOT constitution (the rules in force when the
|
|
806
|
+
// window opened), never today's config — the same rules the server tallies
|
|
807
|
+
// under. Rank KEYS never surface: the display label comes from the branding
|
|
808
|
+
// pack via OTB.rankLabel, as on every other hall surface. The one place a raw
|
|
809
|
+
// predicate still shows is an UNREADABLE one, where the broken string is the
|
|
810
|
+
// actionable fact and hiding it would help nobody.
|
|
811
|
+
function rankNames(keys) {
|
|
812
|
+
const labels = keys.map((k) => escapeHtml(window.OTB.rankLabel(k)));
|
|
813
|
+
if (labels.length === 1) return labels[0];
|
|
814
|
+
return `${labels.slice(0, -1).join(', ')} or ${labels[labels.length - 1]}`;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// A sitting length in the unit a person would say it in. The config stores
|
|
818
|
+
// minutes; "2880 minutes" is a number nobody converts in their head.
|
|
819
|
+
function fmtWindow(minutes) {
|
|
820
|
+
const m = Number(minutes);
|
|
821
|
+
if (!Number.isFinite(m) || m <= 0) return `${escapeHtml(String(minutes))} minutes`;
|
|
822
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
823
|
+
if (m % 1440 === 0) return plural(m / 1440, 'day');
|
|
824
|
+
if (m % 60 === 0) return plural(m / 60, 'hour');
|
|
825
|
+
return plural(m, 'minute');
|
|
757
826
|
}
|
|
758
827
|
|
|
828
|
+
// The short name of a pass rule, for a place a whole sentence will not fit.
|
|
829
|
+
const PASS_RULE_NAME = {
|
|
830
|
+
first_ratifier: 'by first ratifier',
|
|
831
|
+
consent: 'by consent',
|
|
832
|
+
majority: 'by majority',
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const PASS_RULE_PLAIN = {
|
|
836
|
+
first_ratifier: 'The first vote decides it: a yes ratifies, an objection returns it to its author.',
|
|
837
|
+
consent: 'It passes unless someone objects \u2014 silence is agreement, and one reasoned objection returns it.',
|
|
838
|
+
majority: 'It passes only when more than half the board votes yes. Silence is not agreement here.',
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
function decidedByHtml(item) {
|
|
842
|
+
const c = item.constitution || {};
|
|
843
|
+
const past = !!item.closed_at;
|
|
844
|
+
const keys = membershipRankKeys(c.membership);
|
|
845
|
+
const who = keys
|
|
846
|
+
? `Every active <strong>${rankNames(keys)}</strong> ${past ? 'could vote on this' : 'may vote on this'}.`
|
|
847
|
+
: `Nobody ${past ? 'could vote' : 'may vote'} \u2014 the membership setting <code>${escapeHtml(String(c.membership || 'unset'))}</code> cannot be read.`;
|
|
848
|
+
const rule = PASS_RULE_PLAIN[c.pass_rule] || 'How this sitting passes could not be read.';
|
|
849
|
+
const clock = c.window_minutes === null || c.window_minutes === undefined
|
|
850
|
+
? `No deadline \u2014 it ${past ? 'stayed' : 'stays'} open until a vote decides it.`
|
|
851
|
+
: `The sitting ${past ? 'ran' : 'runs'} ${fmtWindow(c.window_minutes)}${c.close_early_on_full_turnout ? ', closing early once every member has voted' : ''}.`;
|
|
852
|
+
const score = item.subject ? item.subject.completeness_score : null;
|
|
853
|
+
const grade = score == null
|
|
854
|
+
? ''
|
|
855
|
+
: `<p class="gov-grid-note">It cleared the completeness grade at ${escapeHtml(String(score))}/100.</p>`;
|
|
856
|
+
return `<p class="gov-grid-note">${who}</p>
|
|
857
|
+
<p class="gov-grid-note">${rule}</p>
|
|
858
|
+
<p class="gov-grid-note">${clock}</p>${grade}`;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// TASK 1003270: the ballot is the point of the card, so its copy earns its
|
|
862
|
+
// place or goes. What stayed is the ADR 0175 §8 requirement itself — an
|
|
863
|
+
// objection carries a REASON and names ONE SECTION — because that requirement
|
|
864
|
+
// is the whole reason voting lives in the hall instead of on a chat reaction.
|
|
865
|
+
// Simplifying may not weaken it, so the reason box and the five-way picker are
|
|
866
|
+
// untouched; only the sentences around them shrank.
|
|
759
867
|
function voteFormHtml(item, viewerId) {
|
|
760
868
|
if (item.closed_at) return '';
|
|
761
869
|
const mine = item.votes.find((v) => String(v.voter_id) === String(viewerId));
|
|
@@ -770,16 +878,16 @@
|
|
|
770
878
|
const isOwnIdea = item.subject_type === 'full_idea'
|
|
771
879
|
&& item.subject && String(item.subject.author_id) === String(viewerId);
|
|
772
880
|
const sections = Object.keys(SECTION_LABELS).map((key) => `
|
|
773
|
-
<label class="gov-perm"
|
|
881
|
+
<label class="gov-perm"><input type="radio" name="vote-section-${escapeHtml(item.id)}" value="${escapeHtml(key)}"> <span>${escapeHtml(SECTION_LABELS[key])}</span></label>`).join('');
|
|
774
882
|
const ownNote = isOwnIdea
|
|
775
|
-
? `<p class="gov-grid-note">
|
|
883
|
+
? `<p class="gov-grid-note">Your own idea. You may vote, but it can\u2019t be decided by your vote alone, and a vote on your own idea earns no karma.</p>`
|
|
776
884
|
: '';
|
|
777
885
|
return `<div class="gov-vote" data-item="${escapeHtml(item.id)}">
|
|
778
886
|
${ownNote}
|
|
779
887
|
<button type="button" class="gov-btn" data-vote="yes">Vote yes</button>
|
|
780
888
|
<details class="gov-vote__no">
|
|
781
889
|
<summary class="gov-btn gov-btn--ghost">Object\u2026</summary>
|
|
782
|
-
<p class="gov-grid-note">
|
|
890
|
+
<p class="gov-grid-note">This sends the idea back to its author. Say why, and which section.</p>
|
|
783
891
|
<textarea class="gov-input" data-vote-reason placeholder="Why \u2014 the reason the author will act on" maxlength="5000" rows="3"></textarea>
|
|
784
892
|
<div class="gov-vote__sections">${sections}</div>
|
|
785
893
|
<button type="button" class="gov-btn" data-vote="no">Cast objection</button>
|
|
@@ -807,27 +915,60 @@
|
|
|
807
915
|
loadBoard(); // re-render either way — the room shows what stands
|
|
808
916
|
}
|
|
809
917
|
|
|
918
|
+
// ONE SITTING, ONE DECISION (task 1003270). The card reads top-down in the
|
|
919
|
+
// order a person actually needs it:
|
|
920
|
+
//
|
|
921
|
+
// 1. what is being decided — the idea's TITLE, at title size. It used to
|
|
922
|
+
// render through `.gov-section__head`, i.e. 11.5px uppercase letterspaced:
|
|
923
|
+
// the subject of the vote styled as a table header.
|
|
924
|
+
// 2. the ACTION, immediately under it. Everything that used to sit between
|
|
925
|
+
// the two — a tally of raw config keys, a sentence explaining that the UI
|
|
926
|
+
// had no vote form (it does), and a collapsed idea — is gone or moved, so
|
|
927
|
+
// "Vote yes" lands above the fold on an ordinary window instead of below
|
|
928
|
+
// a wall of prose.
|
|
929
|
+
// 3. the idea itself, open (ideaSectionsHtml).
|
|
930
|
+
// 4. the constitutional metadata, collapsed — who votes, how it passes, the
|
|
931
|
+
// clock, and the open ballot. Still one click away, never invisible: the
|
|
932
|
+
// tally rides the summary, which is the part of it a reader scans for
|
|
933
|
+
// (ADR 0175 §9 — a board whose votes are invisible constrains nobody).
|
|
934
|
+
//
|
|
935
|
+
// A CLOSED sitting has no action and no idea body: its head carries the
|
|
936
|
+
// outcome and the record lives under the same summary.
|
|
810
937
|
function itemCard(item, viewerId) {
|
|
811
938
|
const yes = item.votes.filter((v) => v.direction === 'yes').length;
|
|
812
939
|
const no = item.votes.filter((v) => v.direction === 'no').length;
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
940
|
+
const isOpen = !item.closed_at;
|
|
941
|
+
const state = isOpen
|
|
942
|
+
? `<span class="gov-pill gov-pill--custom">open</span> <span class="muted">${escapeHtml(clockLine(item))}</span>`
|
|
943
|
+
: `<span class="gov-pill ${item.outcome === 'passed' ? 'gov-pill--seeded' : 'gov-pill--locked'}">${escapeHtml(item.outcome || '')}</span>`;
|
|
816
944
|
const amendmentDetail = item.subject_type === 'constitutional_amendment' && item.subject
|
|
817
|
-
? `<
|
|
945
|
+
? `<p class="gov-item__amend">Proposes <code>${escapeHtml(JSON.stringify(item.subject.proposed))}</code>${item.subject.rationale_md ? ` \u2014 ${escapeHtml(item.subject.rationale_md)}` : ''}</p>`
|
|
818
946
|
: '';
|
|
819
947
|
const mine = item.votes.find((v) => String(v.voter_id) === String(viewerId));
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
948
|
+
// A voter who has cast sees WHAT they cast where the buttons were. The line
|
|
949
|
+
// that used to fill this slot for everyone else — announcing that the reader
|
|
950
|
+
// had not voted and that casting was still to come — is deleted outright: it
|
|
951
|
+
// explained the UI to itself, and the form it promised shipped in R17.
|
|
952
|
+
const action = !isOpen ? ''
|
|
953
|
+
: mine
|
|
954
|
+
? `<p class="gov-item__cast">You ${mine.direction === 'no' ? 'objected' : 'voted yes'}. One vote per member.</p>`
|
|
955
|
+
: voteFormHtml(item, viewerId);
|
|
956
|
+
const tally = `${yes} yes \u00b7 ${no} objection${no === 1 ? '' : 's'}`;
|
|
957
|
+
return `<section class="gov-item" id="board-item-${escapeHtml(item.id)}">
|
|
958
|
+
<div class="gov-item__head">
|
|
959
|
+
<h3 class="gov-item__title">${subjectLine(item)}</h3>
|
|
960
|
+
<span class="gov-item__state">${state}</span>
|
|
961
|
+
</div>
|
|
825
962
|
${amendmentDetail}
|
|
826
|
-
|
|
827
|
-
${
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
963
|
+
${action ? `<div class="gov-item__act">${action}</div>` : ''}
|
|
964
|
+
${isOpen ? ideaSectionsHtml(item) : ''}
|
|
965
|
+
<details class="gov-item__meta">
|
|
966
|
+
<summary class="gov-item__summary">${tally} \u00b7 who votes, and how it ${isOpen ? 'passes' : 'passed'}</summary>
|
|
967
|
+
<div class="gov-item__metabody">
|
|
968
|
+
${decidedByHtml(item)}
|
|
969
|
+
${ballotHtml(item, viewerId)}
|
|
970
|
+
</div>
|
|
971
|
+
</details>
|
|
831
972
|
</section>`;
|
|
832
973
|
}
|
|
833
974
|
|
|
@@ -905,7 +1046,9 @@
|
|
|
905
1046
|
const rank = (me && (me.builder || me).rank || '').toLowerCase();
|
|
906
1047
|
const isArchon = rank === 'archon';
|
|
907
1048
|
|
|
908
|
-
|
|
1049
|
+
// Task 1003270: the subtitle used to restate the three tab labels sitting
|
|
1050
|
+
// directly beneath it, in ~20 words, above everything actionable.
|
|
1051
|
+
document.getElementById('gov-sub').textContent = 'How this project is governed.';
|
|
909
1052
|
document.getElementById('gov-body').hidden = false;
|
|
910
1053
|
wireTabs({ isArchon });
|
|
911
1054
|
loadBoard(); // the room loads for everyone the page admits
|
|
@@ -99,8 +99,8 @@ function esc(s) {
|
|
|
99
99
|
const ACCENT = (() => {
|
|
100
100
|
try {
|
|
101
101
|
const c = api.branding().theme?.ui?.colors || {};
|
|
102
|
-
return { sun: c.accent || '#
|
|
103
|
-
} catch { return { sun: '#
|
|
102
|
+
return { sun: c.accent || '#fb9569', sun600: c.accentDeep || '#a3481a' };
|
|
103
|
+
} catch { return { sun: '#fb9569', sun600: '#a3481a' }; }
|
|
104
104
|
})();
|
|
105
105
|
|
|
106
106
|
// Brand styling for the standalone onboarding pages — sunrise palette + Baloo 2 /
|
|
@@ -108,7 +108,7 @@ const ACCENT = (() => {
|
|
|
108
108
|
// under the instance's own config/branding.json). Light + dark aware; no external
|
|
109
109
|
// dependency beyond an optional Google-Fonts link (system fallback if it doesn't load).
|
|
110
110
|
const PAGE_STYLE = `
|
|
111
|
-
:root{color-scheme:light dark;--sun:${ACCENT.sun};--sun600:${ACCENT.sun600};--coral:#fb7185;--sky:#38bdf8;--sky600:#0284c7;--ok:#16a34a;--warn:#f59e0b;--ink:#2a1f17;--inkSoft:#6b5d52;--card:#fffdfa;--line:#f0e2cf;--bg1:#fde8c8;--bg2:#fff4e6;--bg3:#fffdfa}
|
|
111
|
+
:root{color-scheme:light dark;--sun:${ACCENT.sun};--sun600:${ACCENT.sun600};--sunInk:#2a1305;--coral:#fb7185;--sky:#38bdf8;--sky600:#0284c7;--ok:#16a34a;--warn:#f59e0b;--ink:#2a1f17;--inkSoft:#6b5d52;--card:#fffdfa;--line:#f0e2cf;--bg1:#fde8c8;--bg2:#fff4e6;--bg3:#fffdfa}
|
|
112
112
|
@media (prefers-color-scheme:dark){:root{--ink:#f5efe6;--inkSoft:#cbbdad;--card:#241b13;--line:#3a2c1e;--bg1:#2a1d10;--bg2:#1b130b;--bg3:#130d07}}
|
|
113
113
|
*{box-sizing:border-box}html,body{height:100%}
|
|
114
114
|
body{margin:0;font-family:"Quicksand",system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:linear-gradient(180deg,var(--bg1) 0%,var(--bg2) 45%,var(--bg3) 100%);background-attachment:fixed;display:flex;align-items:center;justify-content:center;padding:1.5rem;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
|
@@ -135,7 +135,7 @@ code{font-family:"Fira Code",ui-monospace,SFMono-Regular,monospace;background:rg
|
|
|
135
135
|
@media (prefers-reduced-motion:reduce){.step.active .dot,.step.active .dot::after{animation:none}}
|
|
136
136
|
.status-line{min-height:1.4em;color:var(--inkSoft);font-size:.94rem;margin:.4rem auto 0;max-width:24rem;line-height:1.45}
|
|
137
137
|
.cta{margin-top:1.4rem}
|
|
138
|
-
.btn{display:inline-block;background:var(--sun);color
|
|
138
|
+
.btn{display:inline-block;background:var(--sun);color:var(--sunInk);text-decoration:none;font-weight:700;font-family:"Baloo 2",system-ui,sans-serif;padding:.72rem 1.5rem;border-radius:999px;font-size:1.02rem;box-shadow:0 10px 22px -8px rgba(249,115,22,.7);transition:transform .15s ease,box-shadow .15s ease}
|
|
139
139
|
.btn:hover{transform:translateY(-1px);box-shadow:0 14px 28px -8px rgba(249,115,22,.8)}
|
|
140
140
|
.muted{color:var(--inkSoft);opacity:.9;font-size:.85rem;margin:1.1rem auto 0;max-width:24rem;line-height:1.45}
|
|
141
141
|
.hidden{display:none!important}
|