diff --git a/licensing-service/src/api/buy_page.rs b/licensing-service/src/api/buy_page.rs index 40423bc..c917338 100644 --- a/licensing-service/src/api/buy_page.rs +++ b/licensing-service/src/api/buy_page.rs @@ -186,7 +186,7 @@ body {{ padding:14px 24px; }} .topbar .inner {{ - max-width:680px; margin:0 auto; + max-width:1040px; margin:0 auto; display:flex; align-items:center; gap:12px; font-family:var(--font-display); font-weight:500; font-size:14px; letter-spacing:0.28em; text-transform:uppercase; color:var(--navy-900); @@ -197,11 +197,21 @@ body {{ color:var(--ink-500); margin-left:auto; }} -.wrap {{ max-width:560px; margin:48px auto; padding:0 24px; }} +/* Outer container width — was 560px (single-column friendly), now + wider so the 3-tier grid below has room to breathe and matches the + admin Policies page layout. Inner text + form blocks are constrained + back to ~560px reading width by the `.wrap > :not(.tiers)` rule + below so only the tier grid breaks out. */ +.wrap {{ max-width:1040px; margin:48px auto; padding:0 24px; }} +.wrap > :not(.tiers) {{ max-width:560px; margin-left:auto; margin-right:auto; }} .eyebrow {{ font-size:11.5px; font-weight:700; letter-spacing:0.18em; text-transform:uppercase; color:var(--gold-700); margin-bottom:14px; - display:inline-flex; align-items:center; gap:10px; + /* `flex; width:fit-content` instead of `inline-flex` so the + wrap-children margin:auto centering rule applies — otherwise + this inline element would sit flush left of the wider 1040px + container while its centered block-level siblings sit middle. */ + display:flex; width:fit-content; align-items:center; gap:10px; }} .eyebrow::before {{ content:''; display:inline-block; width:28px; height:1px; background:var(--gold-500); }} h1 {{ diff --git a/startos/versions/v0.2.0.ts b/startos/versions/v0.2.0.ts index 7d4e679..04e5ccb 100644 --- a/startos/versions/v0.2.0.ts +++ b/startos/versions/v0.2.0.ts @@ -58,6 +58,8 @@ const RELEASE_NOTES = [ // in RELEASE_NOTES above (the milestone). Subsequent revisions // append here. const ROUTINE_NOTES = [ + '0.2.0:21 — **Wider buy page so 3-tier grids breathe.** The public /buy/ page was capped at 560px, which packed three tier cards into a too-narrow column on desktop browsers. Bumped the outer container to 1040px so the tier picker matches the admin Policies page layout. The form, price card, and intro text below the tier picker remain centered at the 560px reading-width so the buy form doesn\'t look stretched. Mobile (≤480px) breakpoint unchanged. Topbar inner widened to match. UI-only; no API or schema change.', + '', '0.2.0:20 — **Discount codes can apply to multiple policies, not just one.** Operator picks a subset (e.g. "Patron AND Pro but not Creator") on a single code instead of cloning the code under different names.', '', '**What changed.** Previously, a discount code\'s tier scope was a single policy (`applies_to_policy_id`) or "any policy on this product" / global. To offer the same discount across two of three tiers required creating two codes with distinct strings — operationally messy and harder for buyers. The form now has a tier multi-select pill picker: click tiers to toggle inclusion. 0 picked = "any policy on this product" (unchanged). 1 picked = single-policy scope (writes to the legacy column for clarity). 2+ picked = the code applies if and only if the chosen tier is in the picked set.', @@ -362,7 +364,7 @@ const ROUTINE_NOTES = [ ].join('\n\n') export const v0_2_0 = VersionInfo.of({ - version: '0.2.0:20', + version: '0.2.0:21', releaseNotes: { en_US: ROUTINE_NOTES }, // No on-disk transformation needed — v0.2.0:0 is a label change. // SQLite-level migrations live separately under