max / makenotwork
7 files changed,
+258 insertions,
-50 deletions
| @@ -237,3 +237,59 @@ chargeback_rate_tier_subs = 0.001 # A12: 0.1% for recurring (lower than one- | |||
| 237 | 237 | # - rho_incident ≤ rho_annual (single decision ≤ annual budget) | |
| 238 | 238 | # - All `tiers.founding[k]` ≤ `tiers.standard[k]` | |
| 239 | 239 | # - cap_count > 0, cap_months > 0 | |
| 240 | + | ||
| 241 | + | ||
| 242 | + | # ─── Per-tier monthly fee allocation (Mode 1 widget on /pricing) ───────── | |
| 243 | + | # | |
| 244 | + | # Each row sums to the standard tier price. The widget renders standard-rate | |
| 245 | + | # numbers only; founder-rate creators pay 50% of standard with the subsidy | |
| 246 | + | # absorbed by reserves + earnback, so the allocation shape is the same but | |
| 247 | + | # the visible total scales down — a one-line note on the pricing page covers | |
| 248 | + | # that case. | |
| 249 | + | # | |
| 250 | + | # Model (per `site-docs/public/about/economics.md`): | |
| 251 | + | # - Stripe: 2.9% × price + $0.30 (Stripe's actual fee formula) | |
| 252 | + | # - Storage: at-rest + egress + virus-scan + backups + provider-price | |
| 253 | + | # cushion; ~5–15× the bare Hetzner at-rest math from | |
| 254 | + | # [creator_marginal] to absorb spikes | |
| 255 | + | # - Support: flat $5.00/creator — per-creator floor, doesn't scale | |
| 256 | + | # with tier (a Basic text creator and a $60 video creator | |
| 257 | + | # each get the same human availability) | |
| 258 | + | # - Engineering: flat $6.00/creator — engineering hours don't scale with | |
| 259 | + | # what tier someone bought; the same platform serves all | |
| 260 | + | # - Reserves: 12.5% of revenue (risk float for refunds/chargebacks/runway) | |
| 261 | + | # - Earn-back: residual; the surplus from higher tiers funds platform | |
| 262 | + | # development that benefits everyone | |
| 263 | + | # | |
| 264 | + | # Numbers below are pinned by `tier_prices::tests::cost_allocation_*`. | |
| 265 | + | [cost_allocation.basic] | |
| 266 | + | stripe = 0.76 | |
| 267 | + | storage = 0.40 | |
| 268 | + | support = 5.00 | |
| 269 | + | engineering = 6.00 | |
| 270 | + | reserves = 2.00 | |
| 271 | + | earnback = 1.84 | |
| 272 | + | ||
| 273 | + | [cost_allocation.small_files] | |
| 274 | + | stripe = 1.00 | |
| 275 | + | storage = 1.00 | |
| 276 | + | support = 5.00 | |
| 277 | + | engineering = 6.00 | |
| 278 | + | reserves = 3.00 | |
| 279 | + | earnback = 8.00 | |
| 280 | + | ||
| 281 | + | [cost_allocation.big_files] | |
| 282 | + | stripe = 1.34 | |
| 283 | + | storage = 3.00 | |
| 284 | + | support = 5.00 | |
| 285 | + | engineering = 6.00 | |
| 286 | + | reserves = 4.50 | |
| 287 | + | earnback = 16.16 | |
| 288 | + | ||
| 289 | + | [cost_allocation.everything] | |
| 290 | + | stripe = 2.04 | |
| 291 | + | storage = 6.00 | |
| 292 | + | support = 5.00 | |
| 293 | + | engineering = 6.00 | |
| 294 | + | reserves = 7.50 | |
| 295 | + | earnback = 33.46 |
| @@ -75,6 +75,7 @@ pub struct AppState { | |||
| 75 | 75 | pub email: EmailClient, | |
| 76 | 76 | pub docs: Arc<DocLoader>, | |
| 77 | 77 | pub tier_prices: tier_prices::TierPrices, | |
| 78 | + | pub cost_allocation: tier_prices::CostAllocation, | |
| 78 | 79 | pub scanner: Option<Arc<ScanPipeline>>, | |
| 79 | 80 | pub webauthn: Arc<Webauthn>, | |
| 80 | 81 | pub syntax: Option<Arc<git::SyntaxHighlighter>>, |
| @@ -318,6 +318,10 @@ async fn main() { | |||
| 318 | 318 | email, | |
| 319 | 319 | docs, | |
| 320 | 320 | tier_prices: makenotwork::tier_prices::TierPrices::from_assumptions(&assumptions), | |
| 321 | + | cost_allocation: { | |
| 322 | + | let tp = makenotwork::tier_prices::TierPrices::from_assumptions(&assumptions); | |
| 323 | + | makenotwork::tier_prices::CostAllocation::from_assumptions(&assumptions, &tp) | |
| 324 | + | }, | |
| 321 | 325 | scanner, | |
| 322 | 326 | webauthn, | |
| 323 | 327 | syntax, |
| @@ -356,6 +356,7 @@ pub(super) async fn pricing_page( | |||
| 356 | 356 | PricingTemplate { | |
| 357 | 357 | csrf_token: get_csrf_token(&session).await, | |
| 358 | 358 | tier_prices: state.tier_prices.clone(), | |
| 359 | + | cost_allocation: state.cost_allocation.clone(), | |
| 359 | 360 | } | |
| 360 | 361 | } | |
| 361 | 362 |
| @@ -768,6 +768,7 @@ pub struct DocIndexTemplate { | |||
| 768 | 768 | pub struct PricingTemplate { | |
| 769 | 769 | pub csrf_token: CsrfTokenOption, | |
| 770 | 770 | pub tier_prices: crate::tier_prices::TierPrices, | |
| 771 | + | pub cost_allocation: crate::tier_prices::CostAllocation, | |
| 771 | 772 | } | |
| 772 | 773 | ||
| 773 | 774 | /// Use cases page showcasing creator types. |
| @@ -138,6 +138,131 @@ impl TierPrices { | |||
| 138 | 138 | } | |
| 139 | 139 | } | |
| 140 | 140 | ||
| 141 | + | /// One segment of the per-tier monthly fee bar. Sized in cents so the | |
| 142 | + | /// template can drop it straight into `flex: <cents>` for a stacked | |
| 143 | + | /// horizontal bar that scales proportionally without per-segment math. | |
| 144 | + | #[derive(Clone, Debug)] | |
| 145 | + | pub struct CostSegment { | |
| 146 | + | /// CSS modifier and toml key — `"stripe"`, `"storage"`, `"support"`, | |
| 147 | + | /// `"engineering"`, `"reserves"`, `"earnback"`. Drives `.cost-bar-seg-*`. | |
| 148 | + | pub kind: &'static str, | |
| 149 | + | /// Human label e.g. `"Stripe processing"`. | |
| 150 | + | pub label: &'static str, | |
| 151 | + | /// Pre-formatted dollar string e.g. `"$0.76"` for the segment + tooltip. | |
| 152 | + | pub amount: String, | |
| 153 | + | /// Amount in cents — the flex weight for the stacked bar. | |
| 154 | + | pub cents: i32, | |
| 155 | + | /// Pre-formatted hover text (rendered into `title="…"`). Kept on the | |
| 156 | + | /// Rust side so copy reviews land here and not in the template. | |
| 157 | + | pub tooltip: String, | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | /// One row of the cost-allocation widget — a single tier and its six | |
| 161 | + | /// fee segments. The template iterates `rows` and per row iterates | |
| 162 | + | /// `segments`, so the markup is fully uniform. | |
| 163 | + | #[derive(Clone, Debug)] | |
| 164 | + | pub struct CostAllocationRow { | |
| 165 | + | pub tier_key: &'static str, | |
| 166 | + | pub tier_label: &'static str, | |
| 167 | + | /// Monthly price in dollars (whole-dollar tiers today). | |
| 168 | + | pub tier_price: i32, | |
| 169 | + | pub segments: Vec<CostSegment>, | |
| 170 | + | /// Pre-built `aria-label` for the bar — names every segment + value | |
| 171 | + | /// so screen readers get the breakdown. | |
| 172 | + | pub aria_label: String, | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | /// All four tier rows, in canonical Basic → Everything order. | |
| 176 | + | #[derive(Clone, Debug, Default)] | |
| 177 | + | pub struct CostAllocation { | |
| 178 | + | pub rows: Vec<CostAllocationRow>, | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | impl CostAllocation { | |
| 182 | + | pub fn from_assumptions(a: &Assumptions, tp: &TierPrices) -> Self { | |
| 183 | + | let rows = vec![ | |
| 184 | + | row(a, "basic", "Basic", tp.basic_std), | |
| 185 | + | row(a, "small_files", "Small Files", tp.small_files_std), | |
| 186 | + | row(a, "big_files", "Big Files", tp.big_files_std), | |
| 187 | + | row(a, "everything", "Everything", tp.everything_std), | |
| 188 | + | ]; | |
| 189 | + | Self { rows } | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | fn row(a: &Assumptions, key: &'static str, label: &'static str, price: i32) -> CostAllocationRow { | |
| 194 | + | let segments = vec![ | |
| 195 | + | seg(a, key, "stripe", "Stripe processing", | |
| 196 | + | "Stripe processing: {amount}. Stripe's fee on your monthly subscription."), | |
| 197 | + | seg(a, key, "storage", "Storage", | |
| 198 | + | "Storage: {amount}. At-rest storage, bandwidth, virus scanning, and \ | |
| 199 | + | backups at your tier's typical fill — sized with headroom for spikes \ | |
| 200 | + | and cloud-provider price changes."), | |
| 201 | + | seg(a, key, "support", "Human support time", | |
| 202 | + | "Human support time: {amount}. Identity recovery, billing disputes, \ | |
| 203 | + | moderation, abuse, and legal — the work that can't be automated. \ | |
| 204 | + | Per-creator floor; the same at every tier."), | |
| 205 | + | seg(a, key, "engineering", "Product engineering", | |
| 206 | + | "Product engineering: {amount}. Bug fixes and ongoing product work. \ | |
| 207 | + | Per-creator floor; the same at every tier."), | |
| 208 | + | seg(a, key, "reserves", "Reserves", | |
| 209 | + | "Reserves: {amount}. Held against a bad month — refunds, chargebacks, \ | |
| 210 | + | unexpected costs — so a single incident doesn't force a price change \ | |
| 211 | + | or shutdown."), | |
| 212 | + | seg(a, key, "earnback", "Earn-back surplus", | |
| 213 | + | "Earn-back surplus: {amount}. The residual after costs. Funds platform \ | |
| 214 | + | development that benefits every tier — bug fixes, new features, and \ | |
| 215 | + | lower future prices as the cost base amortizes across more creators."), | |
| 216 | + | ]; | |
| 217 | + | let aria_label = build_aria_label(label, price, &segments); | |
| 218 | + | CostAllocationRow { | |
| 219 | + | tier_key: key, | |
| 220 | + | tier_label: label, | |
| 221 | + | tier_price: price, | |
| 222 | + | segments, | |
| 223 | + | aria_label, | |
| 224 | + | } | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | fn seg( | |
| 228 | + | a: &Assumptions, | |
| 229 | + | tier_key: &str, | |
| 230 | + | seg_kind: &'static str, | |
| 231 | + | seg_label: &'static str, | |
| 232 | + | tooltip_template: &str, | |
| 233 | + | ) -> CostSegment { | |
| 234 | + | let dollars = float_at(a, &format!("cost_allocation.{tier_key}.{seg_kind}")); | |
| 235 | + | let cents = (dollars * 100.0).round() as i32; | |
| 236 | + | let amount = format!("${dollars:.2}"); | |
| 237 | + | let tooltip = tooltip_template.replace("{amount}", &amount); | |
| 238 | + | CostSegment { | |
| 239 | + | kind: seg_kind, | |
| 240 | + | label: seg_label, | |
| 241 | + | amount, | |
| 242 | + | cents, | |
| 243 | + | tooltip, | |
| 244 | + | } | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | fn build_aria_label(tier_label: &str, price: i32, segments: &[CostSegment]) -> String { | |
| 248 | + | let mut s = format!("{tier_label} tier ${price}/mo allocation: "); | |
| 249 | + | for (i, seg) in segments.iter().enumerate() { | |
| 250 | + | if i > 0 { s.push_str(", "); } | |
| 251 | + | s.push_str(seg.label); | |
| 252 | + | s.push(' '); | |
| 253 | + | s.push_str(&seg.amount); | |
| 254 | + | } | |
| 255 | + | s | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | fn float_at(a: &Assumptions, key: &str) -> f64 { | |
| 259 | + | match a.get(key) { | |
| 260 | + | Some(LookupValue::Float(x)) => *x, | |
| 261 | + | Some(LookupValue::Int(n)) => *n as f64, | |
| 262 | + | other => panic!("expected number at {key}, got {other:?}"), | |
| 263 | + | } | |
| 264 | + | } | |
| 265 | + | ||
| 141 | 266 | fn int_at(a: &Assumptions, key: &str) -> i32 { | |
| 142 | 267 | match a.get(key) { | |
| 143 | 268 | Some(LookupValue::Int(n)) => i32::try_from(*n) | |
| @@ -164,6 +289,64 @@ mod tests { | |||
| 164 | 289 | /// these or flips its type, the panic in `from_assumptions` will fire at | |
| 165 | 290 | /// startup; this test catches it at PR time instead. | |
| 166 | 291 | #[test] | |
| 292 | + | fn cost_allocation_from_canonical_assumptions_sums_to_tier_price() { | |
| 293 | + | // Each row's six segments must sum to the standard tier price. | |
| 294 | + | // A future toml edit that breaks that invariant — say a typo in | |
| 295 | + | // `cost_allocation.big_files.storage` — would render a stacked | |
| 296 | + | // bar whose visible total disagrees with the headline price. | |
| 297 | + | // Catch the divergence at PR time. | |
| 298 | + | let a = Assumptions::load(ASSUMPTIONS_PATH).expect("load canonical toml"); | |
| 299 | + | let tp = TierPrices::from_assumptions(&a); | |
| 300 | + | let alloc = CostAllocation::from_assumptions(&a, &tp); | |
| 301 | + | assert_eq!(alloc.rows.len(), 4); | |
| 302 | + | ||
| 303 | + | let prices = [tp.basic_std, tp.small_files_std, tp.big_files_std, tp.everything_std]; | |
| 304 | + | for (row, &price) in alloc.rows.iter().zip(prices.iter()) { | |
| 305 | + | assert_eq!(row.tier_price, price); | |
| 306 | + | let sum_cents: i32 = row.segments.iter().map(|s| s.cents).sum(); | |
| 307 | + | assert_eq!( | |
| 308 | + | sum_cents, | |
| 309 | + | price * 100, | |
| 310 | + | "{tier} segments sum to {sum_cents}¢ but tier price is ${price} = {expected}¢", | |
| 311 | + | tier = row.tier_label, | |
| 312 | + | expected = price * 100, | |
| 313 | + | ); | |
| 314 | + | // Six canonical segments in canonical order. | |
| 315 | + | let kinds: Vec<&str> = row.segments.iter().map(|s| s.kind).collect(); | |
| 316 | + | assert_eq!( | |
| 317 | + | kinds, | |
| 318 | + | vec!["stripe", "storage", "support", "engineering", "reserves", "earnback"], | |
| 319 | + | ); | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | // Spot-check the per-segment values match what we agreed in the | |
| 323 | + | // economics doc. A drift here means the toml was edited without | |
| 324 | + | // a corresponding rationale update. | |
| 325 | + | let basic = &alloc.rows[0]; | |
| 326 | + | assert_eq!(basic.segments[0].amount, "$0.76"); // stripe | |
| 327 | + | assert_eq!(basic.segments[5].amount, "$1.84"); // earnback | |
| 328 | + | let everything = &alloc.rows[3]; | |
| 329 | + | assert_eq!(everything.segments[2].amount, "$5.00"); // support flat | |
| 330 | + | assert_eq!(everything.segments[3].amount, "$6.00"); // engineering flat | |
| 331 | + | assert_eq!(everything.segments[4].amount, "$7.50"); // reserves 12.5% of $60 | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | #[test] | |
| 335 | + | fn cost_allocation_aria_label_names_every_segment() { | |
| 336 | + | // Screen readers get the full breakdown via aria-label since the | |
| 337 | + | // colored bar carries no native semantics. Pin the format. | |
| 338 | + | let a = Assumptions::load(ASSUMPTIONS_PATH).expect("load canonical toml"); | |
| 339 | + | let tp = TierPrices::from_assumptions(&a); | |
| 340 | + | let alloc = CostAllocation::from_assumptions(&a, &tp); | |
| 341 | + | let aria = &alloc.rows[0].aria_label; | |
| 342 | + | assert!(aria.starts_with("Basic tier $16/mo allocation: ")); | |
| 343 | + | for label in ["Stripe processing", "Storage", "Human support time", | |
| 344 | + | "Product engineering", "Reserves", "Earn-back surplus"] { | |
| 345 | + | assert!(aria.contains(label), "aria-label missing {label}: {aria}"); | |
| 346 | + | } | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | #[test] | |
| 167 | 350 | fn from_canonical_assumptions_populates_every_field() { | |
| 168 | 351 | let a = Assumptions::load(ASSUMPTIONS_PATH).expect("load canonical toml"); | |
| 169 | 352 | let p = TierPrices::from_assumptions(&a); |
| @@ -82,58 +82,22 @@ | |||
| 82 | 82 | <div class="tier-section cost-allocation" id="cost-allocation"> | |
| 83 | 83 | <h2 class="section-label">Where your tier fee goes</h2> | |
| 84 | 84 | <p class="cost-allocation-intro">Per typical creator, monthly. These are platform-wide budget allocations, not per-account accounting. Hover a segment for details.</p> | |
| 85 | + | <p class="cost-allocation-founder-note">Founder-rate creators pay exactly 50% of the standard tier. Per-creator fixed costs (Support, Engineering, Storage, Stripe) stay the same; the discount comes out of Earn-back and Reserves. The shape is unchanged, the totals halve.</p> | |
| 85 | 86 | ||
| 86 | - | {# Basic — $16 = 0.76 + 0.40 + 5.00 + 6.00 + 2.00 + 1.84. Sub-amounts hardcoded; see launchplan §4.5.1 for the planned [cost_allocation] toml section. #} | |
| 87 | + | {# Every dollar here round-trips through `cost_allocation.*` in | |
| 88 | + | `docs/business/assumptions.toml`. Tests in `tier_prices::tests` | |
| 89 | + | pin the per-segment values; tooltip copy lives in the Rust | |
| 90 | + | builder. #} | |
| 91 | + | {% for row in cost_allocation.rows %} | |
| 87 | 92 | <div class="cost-row"> | |
| 88 | - | <div class="cost-row-label"><strong>Basic</strong>${{ tier_prices.basic_std }}/mo</div> | |
| 89 | - | <div class="cost-bar" role="img" aria-label="Basic tier ${{ tier_prices.basic_std }}/mo allocation: Stripe $0.76, Storage $0.40, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $1.84"> | |
| 90 | - | <div class="cost-bar-seg cost-bar-seg-stripe" style="flex: 76" title="Stripe processing: $0.76. Stripe's fee on your monthly subscription.">$0.76</div> | |
| 91 | - | <div class="cost-bar-seg cost-bar-seg-storage" style="flex: 40" title="Storage: $0.40. Object storage at your tier's typical fill, on Hetzner.">$0.40</div> | |
| 92 | - | <div class="cost-bar-seg cost-bar-seg-support" style="flex: 500" title="Human support time: $5.00. Identity recovery, billing disputes, moderation, abuse, and legal — the work that can't be automated.">$5.00</div> | |
| 93 | - | <div class="cost-bar-seg cost-bar-seg-engineering" style="flex: 600" title="Product engineering: $6.00. Bug fixes and ongoing product work. We treat fixing bugs as part of building the product, not as an ongoing support drag.">$6.00</div> | |
| 94 | - | <div class="cost-bar-seg cost-bar-seg-reserves" style="flex: 200" title="Reserves: $2.00. Held against a bad month so a single incident doesn't force a price change or a shutdown.">$2.00</div> | |
| 95 | - | <div class="cost-bar-seg cost-bar-seg-earnback" style="flex: 184" title="Earn-back surplus: $1.84. Earmarked to return to creators as earn-back credit (committed by 2027-01-01).">$1.84</div> | |
| 96 | - | </div> | |
| 97 | - | </div> | |
| 98 | - | ||
| 99 | - | {# Small Files — $24 = 1.00 + 0.60 + 5.00 + 6.00 + 2.00 + 9.40 #} | |
| 100 | - | <div class="cost-row"> | |
| 101 | - | <div class="cost-row-label"><strong>Small Files</strong>${{ tier_prices.small_files_std }}/mo</div> | |
| 102 | - | <div class="cost-bar" role="img" aria-label="Small Files tier ${{ tier_prices.small_files_std }}/mo allocation: Stripe $1.00, Storage $0.60, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $9.40"> | |
| 103 | - | <div class="cost-bar-seg cost-bar-seg-stripe" style="flex: 100" title="Stripe processing: $1.00.">$1.00</div> | |
| 104 | - | <div class="cost-bar-seg cost-bar-seg-storage" style="flex: 60" title="Storage: $0.60. Object storage at your tier's typical fill.">$0.60</div> | |
| 105 | - | <div class="cost-bar-seg cost-bar-seg-support" style="flex: 500" title="Human support time: $5.00.">$5.00</div> | |
| 106 | - | <div class="cost-bar-seg cost-bar-seg-engineering" style="flex: 600" title="Product engineering: $6.00.">$6.00</div> | |
| 107 | - | <div class="cost-bar-seg cost-bar-seg-reserves" style="flex: 200" title="Reserves: $2.00.">$2.00</div> | |
| 108 | - | <div class="cost-bar-seg cost-bar-seg-earnback" style="flex: 940" title="Earn-back surplus: $9.40.">$9.40</div> | |
| 109 | - | </div> | |
| 110 | - | </div> | |
| 111 | - | ||
| 112 | - | {# Big Files — $36 = 1.34 + 0.90 + 5.00 + 6.00 + 2.00 + 20.76 #} | |
| 113 | - | <div class="cost-row"> | |
| 114 | - | <div class="cost-row-label"><strong>Big Files</strong>${{ tier_prices.big_files_std }}/mo</div> | |
| 115 | - | <div class="cost-bar" role="img" aria-label="Big Files tier ${{ tier_prices.big_files_std }}/mo allocation: Stripe $1.34, Storage $0.90, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $20.76"> | |
| 116 | - | <div class="cost-bar-seg cost-bar-seg-stripe" style="flex: 134" title="Stripe processing: $1.34.">$1.34</div> | |
| 117 | - | <div class="cost-bar-seg cost-bar-seg-storage" style="flex: 90" title="Storage: $0.90.">$0.90</div> | |
| 118 | - | <div class="cost-bar-seg cost-bar-seg-support" style="flex: 500" title="Human support time: $5.00.">$5.00</div> | |
| 119 | - | <div class="cost-bar-seg cost-bar-seg-engineering" style="flex: 600" title="Product engineering: $6.00.">$6.00</div> | |
| 120 | - | <div class="cost-bar-seg cost-bar-seg-reserves" style="flex: 200" title="Reserves: $2.00.">$2.00</div> | |
| 121 | - | <div class="cost-bar-seg cost-bar-seg-earnback" style="flex: 2076" title="Earn-back surplus: $20.76.">$20.76</div> | |
| 122 | - | </div> | |
| 123 | - | </div> | |
| 124 | - | ||
| 125 | - | {# Everything — $60 = 2.04 + 1.50 + 5.00 + 6.00 + 2.00 + 43.46 #} | |
| 126 | - | <div class="cost-row"> | |
| 127 | - | <div class="cost-row-label"><strong>Everything</strong>${{ tier_prices.everything_std }}/mo</div> | |
| 128 | - | <div class="cost-bar" role="img" aria-label="Everything tier ${{ tier_prices.everything_std }}/mo allocation: Stripe $2.04, Storage $1.50, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $43.46"> | |
| 129 | - | <div class="cost-bar-seg cost-bar-seg-stripe" style="flex: 204" title="Stripe processing: $2.04.">$2.04</div> | |
| 130 | - | <div class="cost-bar-seg cost-bar-seg-storage" style="flex: 150" title="Storage: $1.50.">$1.50</div> | |
| 131 | - | <div class="cost-bar-seg cost-bar-seg-support" style="flex: 500" title="Human support time: $5.00.">$5.00</div> | |
| 132 | - | <div class="cost-bar-seg cost-bar-seg-engineering" style="flex: 600" title="Product engineering: $6.00.">$6.00</div> | |
| 133 | - | <div class="cost-bar-seg cost-bar-seg-reserves" style="flex: 200" title="Reserves: $2.00.">$2.00</div> | |
| 134 | - | <div class="cost-bar-seg cost-bar-seg-earnback" style="flex: 4346" title="Earn-back surplus: $43.46.">$43.46</div> | |
| 93 | + | <div class="cost-row-label"><strong>{{ row.tier_label }}</strong>${{ row.tier_price }}/mo</div> | |
| 94 | + | <div class="cost-bar" role="img" aria-label="{{ row.aria_label }}"> | |
| 95 | + | {% for seg in row.segments %} | |
| 96 | + | <div class="cost-bar-seg cost-bar-seg-{{ seg.kind }}" style="flex: {{ seg.cents }}" title="{{ seg.tooltip }}">{{ seg.amount }}</div> | |
| 97 | + | {% endfor %} | |
| 135 | 98 | </div> | |
| 136 | 99 | </div> | |
| 100 | + | {% endfor %} | |
| 137 | 101 | ||
| 138 | 102 | <div class="cost-legend" aria-hidden="true"> | |
| 139 | 103 | <span class="cost-legend-item"><span class="cost-legend-swatch cost-bar-seg-stripe"></span>Stripe processing</span> | |
| @@ -143,8 +107,6 @@ | |||
| 143 | 107 | <span class="cost-legend-item"><span class="cost-legend-swatch cost-bar-seg-reserves"></span>Reserves</span> | |
| 144 | 108 | <span class="cost-legend-item"><span class="cost-legend-swatch cost-bar-seg-earnback"></span>Earn-back surplus</span> | |
| 145 | 109 | </div> | |
| 146 | - | ||
| 147 | - | <p class="pricing-disclaimer">Founder-rate creators pay exactly 50% of the standard tier; that subsidy is absorbed by the standard cohort and platform reserves, so the breakdown above applies to standard rates.</p> | |
| 148 | 110 | </div> | |
| 149 | 111 | ||
| 150 | 112 | <div class="landing-cta"> |