feat(deck): populate with real SolLunar Society business plan

Replace the invented financials/entities with the real content from
SolLunarSociety.md — the first node's actual business:

- data.ts: real problem statement; partnership (SPS 45 / Lab 484 45 /
  investors 10, $400k for 10%, $4M valuation); five tenants; three
  membership tiers + Austin comparables; recomputed monthly P&L
  ($65.4k rev / $34k exp / $31.4k profit / $376.8k yr) with the doc's
  non-reconciling summary kept in pnlDocSummary; buildings; investor ask.
- New slides: Partnership (with the Lab 484↔revenue overlap called out),
  Tenants, Memberships, The building, and an interactive P&L drill-down.
  Rewrite the Ask ($400k/10% + profit-share scenarios). Title/Entities
  flag SolLunar as the funded first node; roadmap = festival → farm.
- Remove the invented Revenue/CostBreakdown/Angel/Contributor/
  Investment-overview and the Festivals/Chateau deep-dive slides.
  Ground the vision: SolLunar carries the numbers, the rest stay narrative.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-07-31 19:41:06 +02:00
commit b292b987c3
18 changed files with 684 additions and 599 deletions

View file

@ -10,15 +10,13 @@ import NetworkSlide from './slides/NetworkSlide.vue'
import TechSlide from './slides/TechSlide.vue'
import EntitiesSlide from './slides/EntitiesSlide.vue'
import SolLunarSlide from './slides/SolLunarSlide.vue'
import FestivalsSlide from './slides/FestivalsSlide.vue'
import ChateauSlide from './slides/ChateauSlide.vue'
import PartnershipSlide from './slides/PartnershipSlide.vue'
import TenantsSlide from './slides/TenantsSlide.vue'
import MembershipSlide from './slides/MembershipSlide.vue'
import SpacesSlide from './slides/SpacesSlide.vue'
import GallerySlide from './slides/GallerySlide.vue'
import EcosystemSlide from './slides/EcosystemSlide.vue'
import RevenueSlide from './slides/RevenueSlide.vue'
import InvestmentOverviewSlide from './slides/InvestmentOverviewSlide.vue'
import CostBreakdownSlide from './slides/CostBreakdownSlide.vue'
import AngelSlide from './slides/AngelSlide.vue'
import ContributorSlide from './slides/ContributorSlide.vue'
import PnlSlide from './slides/PnlSlide.vue'
import RoadmapSlide from './slides/RoadmapSlide.vue'
import AskSlide from './slides/AskSlide.vue'
@ -28,16 +26,14 @@ const slides = [
{ id: 'network', label: 'The network', component: NetworkSlide },
{ id: 'archipelago', label: 'Archipelago', component: TechSlide },
{ id: 'entities', label: 'Three experiences', component: EntitiesSlide },
{ id: 'sollunar', label: 'SolLunar Punk Society', component: SolLunarSlide },
{ id: 'festivals', label: 'Pop-up Festivals', component: FestivalsSlide },
{ id: 'chateau', label: 'Chateau du Faune', component: ChateauSlide },
{ id: 'sollunar', label: 'SolLunar Society', component: SolLunarSlide },
{ id: 'partnership', label: 'Ownership', component: PartnershipSlide },
{ id: 'tenants', label: 'Tenants', component: TenantsSlide },
{ id: 'membership', label: 'Memberships', component: MembershipSlide },
{ id: 'spaces', label: 'The building', component: SpacesSlide },
{ id: 'gallery', label: 'Gallery', component: GallerySlide },
{ id: 'ecosystem', label: 'One membership', component: EcosystemSlide },
{ id: 'revenue', label: 'Revenue', component: RevenueSlide },
{ id: 'invest-overview', label: 'The raise', component: InvestmentOverviewSlide },
{ id: 'cost-breakdown', label: 'Cost breakdown', component: CostBreakdownSlide },
{ id: 'angel', label: 'Angel investment', component: AngelSlide },
{ id: 'contribute', label: 'Contribute', component: ContributorSlide },
{ id: 'pnl', label: 'The numbers', component: PnlSlide },
{ id: 'roadmap', label: 'Roadmap', component: RoadmapSlide },
{ id: 'ask', label: 'The ask', component: AskSlide },
]

View file

@ -1,18 +1,19 @@
/**
* ARCHIPELAGO pitch deck content & figures.
*
* EVERYTHING IN THIS FILE IS A PLACEHOLDER.
* This is the single source of truth for the deck. To turn the deck into
* the real pitch, edit values here no slide component hard-codes a number,
* a tier price, or an entity name. Search for `PLACEHOLDER` for every spot
* that needs your real data.
* Single source of truth for the deck no slide hard-codes a number or name.
*
* The financials are REAL, from SolLunarSociety.md, and belong exclusively to
* the first node SolLunar Society (Austin). Archipelago (Lab 484), Pop-up
* Festivals and Chateau du Faune are narrative/vision only and have no
* standalone numbers. P&L totals are recomputed from line items; the doc's
* own summary totals didn't reconcile (kept in `pnlDocSummary`).
*
* Archipelago is the shared backbone open hardware + sovereign software (the
* "mycelium"). Three surface entities run on top of it and share the full core
* stack, so members get the same experience anywhere:
* SolLunar Punk Society city center (members' hub, built on Archipelago)
* Pop-up Festivals travelling gatherings (e.g. Solar Punk Summit)
* Chateau du Faune regenerative farm center
* "mycelium"). Three surface entities run on top of it:
* SolLunar Society city members' club, Austin (the funded first node)
* Pop-up Festivals travelling gatherings (e.g. Solarpunk Summit)
* Chateau du Faune regenerative farm center
*
* Currency: all figures are USD unless noted.
*/
@ -25,24 +26,25 @@ export const brand = {
// a connected network of islands — one backbone, many places.
tagline: 'One network. Three ways to experience it.',
subtagline:
'A collaboration between SolLunar Punk Society, Pop-up Festivals and Chateau du Faune — all running on Archipelago, the shared backbone, so the membership, the payments and the experience travel with you.',
contactEmail: 'invest@archipelago.example', // PLACEHOLDER
'A sovereign network on the Archipelago backbone. The first node — SolLunar Society, a members club in Austin — is live, modeled and raising now; the festival and the farm are next.',
firstNode: 'SolLunar Society · Austin, TX',
contactEmail: 'invest@sollunar.example', // PLACEHOLDER
// Seam for later: an LNbits instance + the founder's nostr npub power the
// "contribute" tiers. Left null until wired (see src/features/{lnbits,nostr}).
lnbitsBaseUrl: null as string | null,
nostrNpub: null as string | null,
}
/** Big-picture framing for the vision slide. */
/** Big-picture framing for the vision slide (from the SolLunar Society plan). */
export const problem = {
headline: 'Connectivity, community, and ownership are centralizing.',
headline: 'Transformation fades without the infrastructure to sustain it.',
points: [
'Networks, payments, and platforms are owned by a handful of gatekeepers.',
'Local communities host the value but capture almost none of it.',
'Every "smart" venue you can name is a walled garden that rents you access.',
'People leave transformational events inspired — then return to environments that dont support the change.',
'Theres no lasting home for sovereignty-minded community: dreamers, creatives, technologists, regenerative living.',
'Peak experiences dont compound without ongoing relationships, education, and a place to practice.',
],
thesis:
'We build sovereign places that own their network, payments and data — a city hub, a travelling festival, and a farm — all on one shared backbone, each proving it can pay for itself.',
'SolLunar Society is the missing infrastructure — a community hub where the work begun at transformational events continues through membership, tenants, events and regenerative living.',
}
export type Accent = 'pine' | 'indigo' | 'rose' | 'clay'
@ -69,20 +71,20 @@ export type Entity = {
export const entities: Entity[] = [
{
id: 'sollunar',
name: 'SolLunar Punk Society',
role: 'City center',
kind: 'Members hub · city',
name: 'SolLunar Society',
role: 'City center · the first node',
kind: 'Members club & community hub · Austin, TX',
accent: 'indigo',
layer: 'surface',
blurb:
'The permanent city home: a members society and event space, built on Archipelagos stack, where the community and the technology live year-round.',
'The first node and the one were raising for: a members club in Austin where sovereignty-minded community — tenants, events, memberships and regenerative living — comes together year-round on the Archipelago stack.',
traits: [
'The permanent, city-based flagship',
'Runs on Archipelago tech — public demonstration of the stack',
'Anchors membership, partnerships and the developer community',
'Live, financially-modeled first node (real P&L)',
'Five founding tenants + a three-tier membership',
'Bitcoin ATM, vending & the full member ecosystem on site',
],
spaces: ['Event hall', 'Co-working floor', 'Exchange kiosk', 'Café & bar', 'Studio / lab'],
revenue: ['Membership', 'Venue & desk hire', 'Events & workshops'],
spaces: ['Retail space', 'Event space', 'Private lounge', 'Co-working', 'Yellow House hostel & data center'],
revenue: ['Memberships', 'Tenants', 'Events & venue buyouts'],
icon: 'Building2',
},
{
@ -217,200 +219,282 @@ export const tech = [
},
]
/** Revenue streams across the collaboration. */
export const revenueStreams = [
{ title: 'Tickets & festivals', note: 'Pop-up festival gatherings', icon: 'Ticket' },
{ title: 'Room & lodging rentals', note: 'City & farm stays', icon: 'BedDouble' },
{ title: 'Retreats & residencies', note: 'Hosted programs at the farm', icon: 'Mountain' },
{ title: 'Experiences & workshops', note: 'Classes, tastings, tours', icon: 'Palette' },
{ title: 'Membership & venue hire', note: 'Society dues, event bookings', icon: 'PartyPopper' },
{ title: 'Network services', note: 'Connectivity, kiosk & payment fees', icon: 'Router' },
/*
SolLunar Society the first node's REAL business (from
SolLunarSociety.md). All figures below are SolLunar Society's
only; Archipelago (Lab 484), Festivals and Chateau have no
standalone numbers yet. Lab 484's economics appear here as
revenue lines INTO SolLunar (~$16k/mo) don't double-count.
*/
/** Ownership & partnership of the SolLunar Society JV. */
export type Partner = {
name: string
short?: string
equity: number // 0..1
accent: Accent | 'amber'
role: string
responsibilities: string[]
note?: string
}
export const partnership = {
valuation: 4_000_000,
raiseAmount: 400_000,
equityOffered: 0.1,
partners: [
{
name: 'SolLunar Punk Society',
short: 'SPS',
equity: 0.45,
accent: 'indigo',
role: 'Space & community',
responsibilities: [
'Curate & decorate the space',
'Lease to tenants · insurance',
'Outbound payments · SOPs',
'Marketing & community management',
],
},
{
name: 'Lab 484',
equity: 0.45,
accent: 'pine',
role: 'Tech & operations',
responsibilities: [
'Operate the spaces & staff',
'All IT / infrastructure (Archipelago)',
'Inbound payments · bootcamps',
'Tenant & investor relations',
],
note: 'Builds & operates the Archipelago backbone — and is itself a tenant + event driver (~$16k/mo of the revenue below).',
},
{
name: 'Investors',
equity: 0.1,
accent: 'amber',
role: 'Capital',
responsibilities: ['$400,000 for 10% equity'],
},
] as Partner[],
}
/** The five founding tenants inside SolLunar Society. */
export const tenants = [
{
name: 'Lab 484',
icon: 'Cpu',
blurb:
'Research lab & startup cluster building a “second internet” — Bitcoin, mesh networks, Nostr, e-cash and P2P. User-owned hardware, software and data.',
},
{
name: 'Solarpunk Summit',
icon: 'Tent',
blurb:
'Transformational platform & community builder — immersive experiences, educational programming and regenerative culture.',
},
{
name: 'Noderunners Shop',
icon: 'Router',
blurb:
'Retail for AI computers, mesh gear, Bitcoin nodes and repair. Open-source, secure laptops, phones and servers.',
},
{
name: 'Divine Provisions',
icon: 'Coins',
blurb:
'A food co-op for low-cost, organic, local groceries — offered as a member service at the building entrance.',
},
{
name: 'Kitchen 484',
icon: 'Sparkles',
blurb:
'Community food truck — organic, seed-oil-free, locally sourced. Runs even when disconnected from the internet.',
},
]
export type CostItem = { label: string; amount: number; note?: string }
export type CostCategory = {
id: string
title: string
subtitle: string
icon: string
accent: Accent
items: CostItem[]
/** Membership tiers (customer product / recurring revenue). */
export type Membership = {
name: string
price: number // monthly
target: number // member-count target
blurb: string
perks: string[]
featured?: boolean
}
export const memberships: Membership[] = [
{
name: 'Solarpunk Society',
price: 50,
target: 150,
blurb: 'Events access.',
perks: ['Member events', 'Amenities, vending & Bitcoin ATM', 'Access to tenants'],
},
{
name: 'Cypherpunk Commons',
price: 350,
target: 50,
featured: true,
blurb: 'Full access, all locations.',
perks: [
'Coworking with the Lab 484 team',
'All locations · tech support',
'Bitcoin ATM, vending, coffee & tea',
'Full tenant & amenity access',
],
},
{
name: 'Lunarpunk Locals',
price: 150,
target: 100,
blurb: 'Coworking days.',
perks: ['Coworking days at SolLunar', 'Access to tenants', 'Amenities, vending & Bitcoin ATM'],
},
]
/** Comparable Austin memberships, for pricing context. */
export const membershipComparables = [
{ name: 'Soho House', price: 350 },
{ name: 'Bond Collective', price: 300 },
{ name: 'Bathe', price: 200 },
{ name: 'Stardust Garage', price: 155 },
{ name: 'Submersive', price: 41 },
]
/**
* Investment cost categories one per entity. PLACEHOLDER amounts (USD).
* The shared Archipelago backbone plus a launch for each experience.
* SolLunar Society monthly P&L. Line items are from the doc; category totals
* are RECOMPUTED from them (the doc's summary totals didn't reconcile see
* `pnlDocSummary`). Flagged oddity: Lunarpunk Locals income ($1,500) doesn't
* match its "100 × $150" annotation.
*/
export const costCategories: CostCategory[] = [
{
id: 'archipelago',
title: 'Archipelago · shared backbone',
subtitle: 'Servers, routers, exchange kiosks, NFC + core dev — powers all three',
icon: 'Cpu',
accent: 'pine',
items: [
{ label: 'Edge servers & compute', amount: 60_000 },
{ label: 'Mesh routers & networking', amount: 45_000 },
{ label: 'Exchange kiosks', amount: 50_000, note: '2 units' },
{ label: 'Membership-card & NFC systems', amount: 25_000 },
{ label: 'Core development salaries', amount: 300_000, note: '4 engineers · 12 mo' },
],
},
{
id: 'sollunar',
title: 'SolLunar Punk Society · city',
subtitle: 'Fit-out & launch of the urban members hub',
icon: 'Building2',
accent: 'indigo',
items: [
{ label: 'Lease deposit & permits', amount: 60_000 },
{ label: 'Renovation & fit-out', amount: 260_000 },
{ label: 'AV, furniture & kitchen', amount: 120_000 },
{ label: 'On-site network install', amount: 40_000 },
{ label: 'Launch & working capital', amount: 40_000 },
],
},
{
id: 'festivals',
title: 'Pop-up Festivals',
subtitle: 'Mobile deployment kit + first-season production',
icon: 'Tent',
accent: 'rose',
items: [
{ label: 'Mobile mesh & power rig', amount: 60_000 },
{ label: 'Mobile exchange kiosk & POS', amount: 45_000 },
{ label: 'Stage, structures & staging gear', amount: 70_000 },
{ label: 'First-season production & launch', amount: 45_000 },
],
},
{
id: 'chateau',
title: 'Chateau du Faune · farm',
subtitle: 'Land, structures & off-grid infrastructure',
icon: 'Trees',
accent: 'clay',
items: [
{ label: 'Land improvements & access', amount: 90_000 },
{ label: 'Lodging & studio structures', amount: 300_000 },
{ label: 'Off-grid power & water', amount: 110_000 },
{ label: 'Alpacas & farm infrastructure', amount: 70_000 },
{ label: 'On-site mesh & launch', amount: 60_000 },
],
},
]
export const categoryTotal = (c: CostCategory) => c.items.reduce((s, i) => s + i.amount, 0)
export const raiseTotal = costCategories.reduce((s, c) => s + categoryTotal(c), 0)
export type AngelTier = {
name: string
minAmount: number
equity: string
benefits: string[]
featured?: boolean
}
/** Angel investment — equity + benefits across the collaboration. PLACEHOLDER terms. */
export const angelTiers: AngelTier[] = [
{
name: 'Seed Angel',
minAmount: 50_000,
equity: 'Equity from a common SAFE',
benefits: [
'Pro-rata equity in the network',
'Founding-member access to all three entities',
'Quarterly investor updates',
],
},
{
name: 'Network Angel',
minAmount: 100_000,
equity: 'Priced equity + pro-rata rights',
featured: true,
benefits: [
'Everything in Seed, plus:',
'Advisory input on network direction',
'Festival, retreat & residency weeks',
'Named node sponsor',
],
},
{
name: 'Founding Angel',
minAmount: 250_000,
equity: 'Priced equity + board observer seat',
benefits: [
'Everything in Network, plus:',
'Board observer seat',
'Naming rights on a flagship space',
'Lifetime network membership',
],
},
]
export type ContributorTier = {
name: string
amount: number
unit?: string
blurb: string
export type PnlItem = { label: string; amount: number; note?: string }
export type PnlGroup = {
id: string
title: string
icon: string
featured?: boolean
accent: Accent
kind: 'revenue' | 'expense'
items: PnlItem[]
}
/** Smaller backers — perks, not equity. PLACEHOLDER prices. */
export const contributorTiers: ContributorTier[] = [
export const pnlGroups: PnlGroup[] = [
{
name: 'Backer',
amount: 25,
blurb: 'Fund a member card and get your name on the founders wall.',
id: 'memberships',
title: 'Memberships',
icon: 'CreditCard',
accent: 'indigo',
kind: 'revenue',
items: [
{ label: 'Cypherpunk Commons', amount: 17_500, note: '50 × $350' },
{ label: 'Solarpunk Society', amount: 7_500, note: '150 × $50' },
{ label: 'Lunarpunk Locals', amount: 1_500, note: '$150/mo · count TBD' },
{ label: 'Vending machine sales', amount: 5_000 },
{ label: 'Promo / B2B network sales', amount: 5_000 },
],
},
{
name: 'Node Router',
amount: 150,
blurb: 'Put a community-owned router on the mesh. Adopt-a-node on the live map.',
icon: 'Router',
id: 'events',
title: 'Events',
icon: 'PartyPopper',
accent: 'rose',
kind: 'revenue',
items: [
{ label: 'Lab 484 tech events', amount: 8_000 },
{ label: 'Lab 484 bootcamps', amount: 6_000 },
{ label: 'Venue buyouts', amount: 6_400, note: '$400/hr · 4hr min · 4×/mo' },
{ label: 'SolLunar Society events', amount: 1_500, note: '$50 weekend passes' },
],
},
{
name: 'Mesh Kit',
amount: 250,
blurb: 'Sponsor a full mesh kit that lights up a location — or a festival.',
icon: 'Wifi',
featured: true,
id: 'tenants',
title: 'Tenants',
icon: 'Building2',
accent: 'pine',
kind: 'revenue',
items: [
{ label: 'Solarpunk Summit', amount: 2_000 },
{ label: 'Lab 484', amount: 2_000 },
{ label: 'Divine Provisions', amount: 1_500 },
{ label: 'Noderunners Shop', amount: 1_500 },
],
},
{
name: 'Sponsor an Alpaca',
amount: 500,
unit: '/yr',
blurb: 'Name & sponsor an alpaca at Chateau du Faune. Photos, updates, and a visit.',
icon: 'Origami',
id: 'expenses',
title: 'Expenses',
icon: 'Coins',
accent: 'clay',
kind: 'expense',
items: [
{ label: 'Staff', amount: 22_500, note: '4 staff' },
{ label: 'Rent', amount: 6_500, note: '$4,500 + triple net' },
{ label: 'Overhead', amount: 3_000 },
{ label: 'Utilities', amount: 2_000, note: 'electric, water, trash' },
],
},
]
/** Roadmap / replication story. */
export const groupTotal = (g: PnlGroup) => g.items.reduce((s, i) => s + i.amount, 0)
const sumKind = (k: 'revenue' | 'expense') =>
pnlGroups.filter((g) => g.kind === k).reduce((s, g) => s + groupTotal(g), 0)
/** Recomputed monthly/yearly economics for SolLunar Society. */
export const pnl = {
monthlyRevenue: sumKind('revenue'),
monthlyExpenses: sumKind('expense'),
monthlyProfit: sumKind('revenue') - sumKind('expense'),
yearlyProfit: (sumKind('revenue') - sumKind('expense')) * 12,
}
/** What the source doc's summary table stated (did NOT reconcile with line items). */
export const pnlDocSummary = { revenue: 71_400, expenses: 38_500, profit: 32_900, yearlyProfit: 394_800 }
/** The building & spaces. */
export const buildings = [
{
name: 'SolLunar Society',
rooms: ['Retail space', 'Event space', 'Private office', 'Private lounge'],
},
{
name: 'Lab 484 · Yellow House 1',
rooms: ['Hostel room · $40/night', 'Healer room', 'Coworking', 'Meeting room', 'Outdoor'],
},
{
name: 'Lab 484 · Yellow House 2',
rooms: ['VIP guest room', 'Office', 'Data center', 'Outdoor'],
},
]
/** The investor ask. */
export const investorAsk = {
amount: 400_000,
equity: 0.1,
valuation: 4_000_000,
scenarios: [0.1, 0.25], // profit-share scenarios on the recomputed yearly profit
}
/** Roadmap / replication story. SolLunar Society first, then the network. */
export const roadmap = [
{
phase: 'Phase 0',
title: 'Raise & finalize',
title: 'Raise & open SolLunar',
when: 'Now',
body: 'Close the angel round, lock sites, finalize the open bill of materials.',
body: 'Close the $400k round, sign the lease, fit out the space and onboard the founding tenants.',
},
{
phase: 'Phase 1',
title: 'Launch the three entities',
when: '~69 mo',
body: 'Open the SolLunar Punk Society city hub and Chateau du Faune; stage the first pop-up festival.',
title: 'Operate & prove the model',
when: '~012 mo',
body: 'Run memberships, events and tenants in Austin; validate the P&L publicly on the Archipelago stack.',
},
{
phase: 'Phase 2',
title: 'Operate & prove revenue',
when: '~918 mo',
body: 'Run events, festivals, retreats and residencies; validate the unit economics publicly.',
title: 'Add the festival',
when: '~1218 mo',
body: 'Deploy the same stack to a travelling Solarpunk Summit — proving the network is portable, not tied to a building.',
},
{
phase: 'Phase 3',
title: 'Open the blueprint',
title: 'Add the farm & replicate',
when: '18 mo+',
body: 'Publish the replication kit; onboard new nodes & festivals worldwide on shared infrastructure.',
body: 'Open Chateau du Faune and publish the replication kit — new nodes plug into the same backbone anywhere.',
},
]

View file

@ -1,52 +0,0 @@
<script setup lang="ts">
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import PlaceholderBadge from '../components/PlaceholderBadge.vue'
import { angelTiers, fmtUsd } from '../data'
</script>
<template>
<SlideFrame
kicker="Ways to invest · Angels"
title="Equity, benefits, and a seat at the table"
subtitle="Angel investors are who we're after — capital plus belief, rewarded with ownership and lifelong access."
>
<div class="grid gap-5 md:grid-cols-3">
<div
v-for="tier in angelTiers"
:key="tier.name"
class="relative flex flex-col rounded-2xl border p-6"
:class="
tier.featured
? 'border-amber-400/50 bg-amber-400/[0.06]'
: 'border-white/10 bg-white/[0.03]'
"
>
<span
v-if="tier.featured"
class="absolute -top-3 left-6 rounded-full bg-amber-400 px-3 py-0.5 text-[11px] font-semibold uppercase tracking-wider text-ink"
>
Most popular
</span>
<h3 class="font-display text-xl font-semibold text-sand">{{ tier.name }}</h3>
<div class="mt-2 flex items-baseline gap-1">
<span class="text-sm text-sand-dim">from</span>
<span class="font-display text-3xl font-bold text-sand">{{ fmtUsd(tier.minAmount) }}</span>
</div>
<p class="mt-1 text-xs font-medium text-pine-300">{{ tier.equity }}</p>
<ul class="mt-5 flex-1 space-y-2.5">
<li v-for="b in tier.benefits" :key="b" class="flex items-start gap-2 text-sm text-sand-dim">
<Icon name="Check" class="mt-0.5 size-4 shrink-0 text-pine-300" />
{{ b }}
</li>
</ul>
</div>
</div>
<p class="mt-5 flex items-center gap-2 text-xs text-sand-dim">
<PlaceholderBadge label="terms" /> Amounts, equity structure & benefits are indicative final terms per the round.
</p>
</SlideFrame>
</template>

View file

@ -1,26 +1,42 @@
<script setup lang="ts">
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import PlaceholderBadge from '../components/PlaceholderBadge.vue'
import { brand, raiseTotal, fmtUsd } from '../data'
import { brand, investorAsk, pnl, fmtUsd } from '../data'
const pct = (n: number) => Math.round(n * 100)
// annual profit-share per scenario, on the recomputed yearly profit
const shares = investorAsk.scenarios.map((s) => ({ equity: s, annual: Math.round(pnl.yearlyProfit * s) }))
</script>
<template>
<SlideFrame center>
<p class="text-xs font-semibold uppercase tracking-[0.3em] text-amber-400">The ask</p>
<h2 class="mt-4 font-display text-4xl font-semibold text-sand sm:text-5xl">
Help us grow the first three flowers
{{ fmtUsd(investorAsk.amount) }} for {{ pct(investorAsk.equity) }}% of SolLunar Society
</h2>
<div class="mt-8 flex items-baseline justify-center gap-3">
<span class="font-display text-6xl font-bold text-sand">{{ fmtUsd(raiseTotal) }}</span>
<PlaceholderBadge />
</div>
<p class="mt-2 max-w-xl text-sand-dim">
to launch {{ brand.name }} an angel round for equity partners, open to backers of every size.
<p class="mt-3 text-sand-dim">
{{ fmtUsd(investorAsk.valuation) }} valuation · the first node, open in Austin
</p>
<div class="mt-10 flex flex-col items-center gap-3 sm:flex-row">
<!-- profit-share scenarios -->
<div class="mt-8 flex flex-wrap items-stretch justify-center gap-4">
<div
v-for="s in shares"
:key="s.equity"
class="rounded-2xl border border-white/10 bg-white/[0.03] px-6 py-4"
>
<p class="text-xs uppercase tracking-wider text-sand-dim">{{ pct(s.equity) }}% share</p>
<p class="mt-1 font-display text-3xl font-bold text-sand">{{ fmtUsd(s.annual) }}</p>
<p class="text-xs text-sand-dim">/ yr profit</p>
</div>
<div class="rounded-2xl border border-pine-500/40 bg-pine-500/[0.07] px-6 py-4">
<p class="text-xs uppercase tracking-wider text-pine-300">Node profit</p>
<p class="mt-1 font-display text-3xl font-bold text-sand">{{ fmtUsd(pnl.yearlyProfit) }}</p>
<p class="text-xs text-sand-dim">/ yr (recomputed)</p>
</div>
</div>
<div class="mt-9 flex flex-col items-center gap-3 sm:flex-row">
<a
:href="`mailto:${brand.contactEmail}`"
class="inline-flex items-center gap-2 rounded-full bg-amber-400 px-6 py-3 font-semibold text-ink transition hover:bg-amber-300"
@ -36,6 +52,8 @@ import { brand, raiseTotal, fmtUsd } from '../data'
</button>
</div>
<p class="mt-10 font-display text-lg text-pine-300">{{ brand.tagline }}</p>
<p class="mt-9 max-w-xl text-sm text-sand-dim">
Prove the model in Austin, then replicate it on Archipelago the festival and the farm are next.
</p>
</SlideFrame>
</template>

View file

@ -1,8 +0,0 @@
<script setup lang="ts">
import EntityDetail from '../components/EntityDetail.vue'
import { entityById } from '../data'
</script>
<template>
<EntityDetail :entity="entityById('chateau')" />
</template>

View file

@ -1,49 +0,0 @@
<script setup lang="ts">
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import { contributorTiers, brand, fmtUsdFull } from '../data'
// Seam: when brand.lnbitsBaseUrl is set, these become live Lightning
// checkouts (see src/features/lnbits). Until then they're "coming soon".
const live = !!brand.lnbitsBaseUrl
</script>
<template>
<SlideFrame
kicker="Ways to invest · Everyone else"
title="Own a piece of the network for the price of dinner"
subtitle="You don't need to be an angel to build this. Back a specific piece of hardware — or an alpaca."
>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div
v-for="tier in contributorTiers"
:key="tier.name"
class="flex flex-col rounded-2xl border p-5"
:class="tier.featured ? 'border-pine-500/50 bg-pine-500/[0.06]' : 'border-white/10 bg-white/[0.03]'"
>
<span class="grid size-11 place-items-center rounded-xl bg-white/5 text-pine-300">
<Icon :name="tier.icon" class="size-5" />
</span>
<div class="mt-4 flex items-baseline gap-1">
<span class="font-display text-2xl font-bold text-sand">{{ fmtUsdFull(tier.amount) }}</span>
<span v-if="tier.unit" class="text-sm text-sand-dim">{{ tier.unit }}</span>
</div>
<h3 class="mt-1 font-display text-base font-semibold text-sand">{{ tier.name }}</h3>
<p class="mt-1.5 flex-1 text-sm text-sand-dim">{{ tier.blurb }}</p>
<button
class="mt-4 flex items-center justify-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm font-medium text-sand transition hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-60"
:disabled="!live"
>
<Icon name="Zap" class="size-4 text-amber-300" />
{{ live ? 'Contribute' : 'Coming soon' }}
</button>
</div>
</div>
<p class="mt-5 flex items-center gap-2 text-xs text-sand-dim">
<Icon name="Zap" class="size-3.5 text-amber-300" />
Contributions settle over Bitcoin Lightning wired to LNbits &amp; nostr when we go live.
</p>
</SlideFrame>
</template>

View file

@ -1,55 +0,0 @@
<script setup lang="ts">
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import PlaceholderBadge from '../components/PlaceholderBadge.vue'
import { costCategories, categoryTotal, fmtUsd } from '../data'
const text: Record<string, string> = {
pine: 'text-pine-300',
indigo: 'text-indigo-300',
rose: 'text-rose-300',
clay: 'text-clay-400',
}
</script>
<template>
<SlideFrame
kicker="The raise · line by line"
title="Cost breakdown"
subtitle="Every figure lives in one file — swap placeholders for your real budget in src/deck/data.ts."
>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<div
v-for="c in costCategories"
:key="c.id"
class="flex flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-4"
>
<div class="flex items-center gap-2">
<Icon :name="c.icon" class="size-4 shrink-0" :class="text[c.accent]" />
<h3 class="font-display text-sm font-semibold leading-tight text-sand">{{ c.title }}</h3>
</div>
<ul class="mt-3 flex-1 space-y-2">
<li v-for="item in c.items" :key="item.label" class="text-[13px]">
<div class="flex items-baseline justify-between gap-2">
<span class="text-sand-dim">{{ item.label }}</span>
<span class="shrink-0 tabular-nums text-sand">{{ fmtUsd(item.amount) }}</span>
</div>
<p v-if="item.note" class="text-[10px] text-sand-dim/70">{{ item.note }}</p>
</li>
</ul>
<div class="mt-3 flex items-center justify-between border-t border-white/10 pt-2.5">
<span class="text-[10px] uppercase tracking-wider text-sand-dim">Subtotal</span>
<span class="font-display text-base font-semibold" :class="text[c.accent]">
{{ fmtUsd(categoryTotal(c)) }}
</span>
</div>
</div>
</div>
<p class="mt-4 flex items-center gap-2 text-xs text-sand-dim">
<PlaceholderBadge /> All amounts are illustrative pending your real quotes &amp; salary bands.
</p>
</SlideFrame>
</template>

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import { userEcosystem, entities } from '../data'
import { userEcosystem, surfaceEntities as entities } from '../data'
</script>
<template>

View file

@ -10,15 +10,21 @@ const archipelago = entityById('archipelago')
<SlideFrame
kicker="The collaboration"
title="Three experiences, one backbone"
subtitle="Deliberately different — a city hub, a travelling festival, a farm — each with its own character, all running on Archipelago."
subtitle="One is funded and opening now — SolLunar Society in Austin. The festival and the farm follow, on the same Archipelago stack."
>
<div class="grid gap-5 md:grid-cols-3">
<div
v-for="e in surfaceEntities"
:key="e.id"
class="flex flex-col rounded-2xl border p-6"
:class="accentRing[e.accent]"
class="relative flex flex-col rounded-2xl border p-6"
:class="e.id === 'sollunar' ? 'border-indigo-500/60 bg-indigo-500/[0.08]' : accentRing[e.accent]"
>
<span
v-if="e.id === 'sollunar'"
class="absolute -top-3 left-6 rounded-full bg-indigo-400 px-3 py-0.5 text-[11px] font-semibold uppercase tracking-wider text-ink"
>
Raising now
</span>
<div class="flex items-center gap-3">
<span class="grid size-11 place-items-center rounded-xl bg-white/5" :class="accentText[e.accent]">
<Icon :name="e.icon" class="size-6" />
@ -32,8 +38,8 @@ const archipelago = entityById('archipelago')
<p class="mt-4 flex-1 text-sm leading-relaxed text-sand-dim">{{ e.blurb }}</p>
<div class="mt-5 flex items-center gap-2 border-t border-white/10 pt-4 text-xs text-sand-dim">
<Icon name="Layers" class="size-4 text-amber-300" />
Full membership &amp; ecosystem
<Icon :name="e.id === 'sollunar' ? 'Check' : 'Layers'" class="size-4" :class="e.id === 'sollunar' ? 'text-indigo-300' : 'text-amber-300'" />
{{ e.id === 'sollunar' ? 'Live P&amp;L · this raise' : 'Vision · on the roadmap' }}
</div>
</div>
</div>

View file

@ -1,8 +0,0 @@
<script setup lang="ts">
import EntityDetail from '../components/EntityDetail.vue'
import { entityById } from '../data'
</script>
<template>
<EntityDetail :entity="entityById('festivals')" />
</template>

View file

@ -1,194 +0,0 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import PlaceholderBadge from '../components/PlaceholderBadge.vue'
import { costCategories, categoryTotal, raiseTotal, fmtUsd } from '../data'
// per-category colour (includes the shared 'amber' core-stack category)
const barBg: Record<string, string> = {
pine: 'bg-pine-500',
indigo: 'bg-indigo-500',
rose: 'bg-rose-500',
clay: 'bg-clay-400',
}
const text: Record<string, string> = {
pine: 'text-pine-300',
indigo: 'text-indigo-300',
rose: 'text-rose-300',
clay: 'text-clay-400',
}
const ring: Record<string, string> = {
pine: 'border-pine-500/50 bg-pine-500/[0.06]',
indigo: 'border-indigo-500/50 bg-indigo-500/[0.06]',
rose: 'border-rose-500/50 bg-rose-500/[0.06]',
clay: 'border-clay-400/50 bg-clay-400/[0.06]',
}
// interactive drill-down: which category is expanded (null = overview)
const selected = ref<string | null>(null)
const selectedCat = computed(() => costCategories.find((c) => c.id === selected.value) ?? null)
function toggle(id: string) {
selected.value = selected.value === id ? null : id
}
const pct = (c: (typeof costCategories)[number]) => (categoryTotal(c) / raiseTotal) * 100
</script>
<template>
<SlideFrame
kicker="The raise"
title="Where the investment goes"
subtitle="Tap any category to break down its costs."
>
<div class="grid items-start gap-10 lg:grid-cols-[1fr_1.05fr]">
<!-- left: headline number, proportion bar, live detail panel -->
<div>
<div class="flex items-end gap-3">
<span class="font-display text-6xl font-bold text-sand">{{ fmtUsd(raiseTotal) }}</span>
<PlaceholderBadge />
</div>
<p class="mt-2 text-sand-dim">
One shared core stack, plus a launch for each of the three entities.
</p>
<!-- proportion bar doubles as a selector -->
<div class="mt-8 flex h-4 w-full overflow-hidden rounded-full">
<button
v-for="c in costCategories"
:key="c.id"
class="h-full cursor-pointer transition-opacity"
:class="[barBg[c.accent], selected && selected !== c.id ? 'opacity-30' : 'opacity-100']"
:style="{ width: `${pct(c)}%` }"
:title="`${c.title} · ${fmtUsd(categoryTotal(c))}`"
:aria-label="`Show ${c.title} breakdown`"
@click="toggle(c.id)"
/>
</div>
<div class="mt-3 flex flex-wrap gap-x-5 gap-y-1 text-xs text-sand-dim">
<span v-for="c in costCategories" :key="c.id" class="flex items-center gap-2">
<span class="size-2 rounded-full" :class="barBg[c.accent]" />
{{ Math.round(pct(c)) }}%
</span>
</div>
<!-- detail panel: swaps between hint and the selected breakdown -->
<Transition name="detail" mode="out-in">
<div
v-if="selectedCat"
:key="selectedCat.id"
class="mt-6 rounded-2xl border p-5"
:class="ring[selectedCat.accent]"
>
<div class="flex items-center gap-3">
<span class="grid size-9 place-items-center rounded-lg bg-white/5" :class="text[selectedCat.accent]">
<Icon :name="selectedCat.icon" class="size-5" />
</span>
<div class="min-w-0 flex-1">
<h3 class="font-display text-base font-semibold leading-tight text-sand">
{{ selectedCat.title }}
</h3>
<p class="truncate text-xs text-sand-dim">{{ selectedCat.subtitle }}</p>
</div>
<button
class="grid size-7 shrink-0 place-items-center rounded-full border border-white/10 text-sand-dim transition hover:bg-white/10"
aria-label="Close breakdown"
@click="selected = null"
>
<Icon name="ArrowRight" class="size-4 rotate-180" />
</button>
</div>
<ul class="mt-4 space-y-2">
<li
v-for="item in selectedCat.items"
:key="item.label"
class="flex items-baseline justify-between gap-3 text-sm"
>
<span class="text-sand-dim">
{{ item.label }}
<span v-if="item.note" class="text-[11px] text-sand-dim/60">· {{ item.note }}</span>
</span>
<span class="shrink-0 tabular-nums text-sand">{{ fmtUsd(item.amount) }}</span>
</li>
</ul>
<div class="mt-4 flex items-center justify-between border-t border-white/10 pt-3">
<span class="text-xs uppercase tracking-wider text-sand-dim">Subtotal</span>
<span class="font-display text-lg font-semibold" :class="text[selectedCat.accent]">
{{ fmtUsd(categoryTotal(selectedCat)) }}
</span>
</div>
</div>
<div
v-else
key="hint"
class="mt-6 flex items-center gap-3 rounded-2xl border border-dashed border-white/15 p-5 text-sm text-sand-dim"
>
<Icon name="ArrowRight" class="size-4 shrink-0 text-amber-300" />
Select a category to see its detailed line-item breakdown.
</div>
</Transition>
</div>
<!-- right: four selectable category cards -->
<div class="space-y-2.5">
<button
v-for="c in costCategories"
:key="c.id"
class="flex w-full items-center gap-4 rounded-2xl border p-4 text-left transition"
:class="
selected === c.id
? ring[c.accent]
: 'border-white/10 bg-white/[0.03] hover:border-white/25 hover:bg-white/[0.06]'
"
:aria-pressed="selected === c.id"
@click="toggle(c.id)"
>
<span class="grid size-10 shrink-0 place-items-center rounded-xl bg-white/5" :class="text[c.accent]">
<Icon :name="c.icon" class="size-5" />
</span>
<div class="min-w-0 flex-1">
<h3 class="font-display text-base font-semibold text-sand">{{ c.title }}</h3>
<p class="truncate text-xs text-sand-dim">{{ c.subtitle }}</p>
</div>
<span class="shrink-0 font-display text-lg font-semibold text-sand">
{{ fmtUsd(categoryTotal(c)) }}
</span>
<Icon
name="ChevronRight"
class="size-4 shrink-0 text-sand-dim transition-transform"
:class="selected === c.id ? 'rotate-90' : ''"
/>
</button>
<p class="pt-1 text-center text-[11px] text-sand-dim/70">
{{ costCategories.length }} categories · {{ fmtUsd(raiseTotal) }} total
</p>
</div>
</div>
</SlideFrame>
</template>
<style scoped>
.detail-enter-active,
.detail-leave-active {
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.detail-enter-from,
.detail-leave-to {
opacity: 0;
transform: translateY(6px);
}
@media (prefers-reduced-motion: reduce) {
.detail-enter-active,
.detail-leave-active {
transition: opacity 0.15s ease;
}
.detail-enter-from,
.detail-leave-to {
transform: none;
}
}
</style>

View file

@ -0,0 +1,58 @@
<script setup lang="ts">
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import { memberships, membershipComparables, fmtUsd } from '../data'
// display low high price
const tiers = [...memberships].sort((a, b) => a.price - b.price)
</script>
<template>
<SlideFrame
kicker="SolLunar Society · memberships"
title="Three ways to belong"
subtitle="Recurring membership is the core of the model — priced right for the Austin market."
>
<div class="grid gap-5 md:grid-cols-3">
<div
v-for="t in tiers"
:key="t.name"
class="relative flex flex-col rounded-2xl border p-6"
:class="t.featured ? 'border-indigo-500/50 bg-indigo-500/[0.07]' : 'border-white/10 bg-white/[0.03]'"
>
<span
v-if="t.featured"
class="absolute -top-3 left-6 rounded-full bg-indigo-400 px-3 py-0.5 text-[11px] font-semibold uppercase tracking-wider text-ink"
>
Flagship
</span>
<h3 class="font-display text-xl font-semibold text-sand">{{ t.name }}</h3>
<div class="mt-2 flex items-baseline gap-1">
<span class="font-display text-4xl font-bold text-sand">{{ fmtUsd(t.price) }}</span>
<span class="text-sm text-sand-dim">/mo</span>
</div>
<p class="mt-1 text-xs text-sand-dim">{{ t.blurb }} · target {{ t.target }} members</p>
<ul class="mt-5 flex-1 space-y-2">
<li v-for="p in t.perks" :key="p" class="flex items-start gap-2 text-sm text-sand-dim">
<Icon name="Check" class="mt-0.5 size-4 shrink-0 text-indigo-300" />
{{ p }}
</li>
</ul>
</div>
</div>
<!-- comparables -->
<div class="mt-6 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-sand-dim">
<span class="uppercase tracking-wider">Austin comparables</span>
<span
v-for="c in membershipComparables"
:key="c.name"
class="rounded-full border border-white/10 bg-white/5 px-3 py-1"
>
{{ c.name }} <span class="text-sand">{{ fmtUsd(c.price) }}</span>
</span>
</div>
</SlideFrame>
</template>

View file

@ -0,0 +1,78 @@
<script setup lang="ts">
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import { partnership, fmtUsd } from '../data'
const barBg: Record<string, string> = {
indigo: 'bg-indigo-500',
pine: 'bg-pine-500',
amber: 'bg-amber-400',
}
const text: Record<string, string> = {
indigo: 'text-indigo-300',
pine: 'text-pine-300',
amber: 'text-amber-300',
}
const ring: Record<string, string> = {
indigo: 'border-indigo-500/40 bg-indigo-500/[0.06]',
pine: 'border-pine-500/40 bg-pine-500/[0.06]',
amber: 'border-amber-400/40 bg-amber-400/[0.06]',
}
const pct = (e: number) => Math.round(e * 100)
</script>
<template>
<SlideFrame
kicker="Ownership"
title="A three-way partnership"
subtitle="SolLunar Society is a joint venture. Investors buy in alongside the two operating partners."
>
<!-- equity bar -->
<div class="flex h-5 w-full overflow-hidden rounded-full">
<div
v-for="p in partnership.partners"
:key="p.name"
class="flex h-full items-center justify-center text-[11px] font-semibold text-ink"
:class="barBg[p.accent]"
:style="{ width: `${pct(p.equity)}%` }"
>
{{ pct(p.equity) }}%
</div>
</div>
<div class="mt-6 grid gap-4 md:grid-cols-3">
<div
v-for="p in partnership.partners"
:key="p.name"
class="flex flex-col rounded-2xl border p-5"
:class="ring[p.accent]"
>
<div class="flex items-baseline justify-between gap-2">
<h3 class="font-display text-lg font-semibold text-sand">{{ p.name }}</h3>
<span class="font-display text-2xl font-bold" :class="text[p.accent]">{{ pct(p.equity) }}%</span>
</div>
<p class="text-xs uppercase tracking-wider" :class="text[p.accent]">{{ p.role }}</p>
<ul class="mt-4 flex-1 space-y-1.5">
<li v-for="r in p.responsibilities" :key="r" class="flex items-start gap-2 text-sm text-sand-dim">
<Icon name="Check" class="mt-0.5 size-3.5 shrink-0" :class="text[p.accent]" />
{{ r }}
</li>
</ul>
<p v-if="p.note" class="mt-3 border-t border-white/10 pt-3 text-[11px] leading-relaxed text-sand-dim">
{{ p.note }}
</p>
</div>
</div>
<div class="mt-5 flex flex-wrap items-center justify-center gap-x-6 gap-y-1 text-sm text-sand-dim">
<span>The ask:</span>
<span class="font-display text-lg font-semibold text-sand">
{{ fmtUsd(partnership.raiseAmount) }} for {{ pct(partnership.equityOffered) }}%
</span>
<span>·</span>
<span>{{ fmtUsd(partnership.valuation) }} valuation</span>
</div>
</SlideFrame>
</template>

View file

@ -0,0 +1,166 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import { pnlGroups, groupTotal, pnl, pnlDocSummary, fmtUsd } from '../data'
const barBg: Record<string, string> = {
pine: 'bg-pine-500',
indigo: 'bg-indigo-500',
rose: 'bg-rose-500',
clay: 'bg-clay-400',
}
const text: Record<string, string> = {
pine: 'text-pine-300',
indigo: 'text-indigo-300',
rose: 'text-rose-300',
clay: 'text-clay-400',
}
const ring: Record<string, string> = {
pine: 'border-pine-500/50 bg-pine-500/[0.06]',
indigo: 'border-indigo-500/50 bg-indigo-500/[0.06]',
rose: 'border-rose-500/50 bg-rose-500/[0.06]',
clay: 'border-clay-400/50 bg-clay-400/[0.06]',
}
const revenueGroups = pnlGroups.filter((g) => g.kind === 'revenue')
const selected = ref<string | null>(null)
const selectedGroup = computed(() => pnlGroups.find((g) => g.id === selected.value) ?? null)
function toggle(id: string) {
selected.value = selected.value === id ? null : id
}
const revPct = (id: string) =>
(groupTotal(pnlGroups.find((g) => g.id === id)!) / pnl.monthlyRevenue) * 100
</script>
<template>
<SlideFrame
kicker="SolLunar Society · unit economics"
title="The numbers"
subtitle="Real monthly P&L for the Austin node. Tap any category to break it down."
>
<!-- headline stats -->
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div class="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<p class="text-xs uppercase tracking-wider text-sand-dim">Revenue / mo</p>
<p class="mt-1 font-display text-2xl font-bold text-sand">{{ fmtUsd(pnl.monthlyRevenue) }}</p>
</div>
<div class="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<p class="text-xs uppercase tracking-wider text-sand-dim">Expenses / mo</p>
<p class="mt-1 font-display text-2xl font-bold text-sand">{{ fmtUsd(pnl.monthlyExpenses) }}</p>
</div>
<div class="rounded-2xl border border-pine-500/40 bg-pine-500/[0.07] p-4">
<p class="text-xs uppercase tracking-wider text-pine-300">Profit / mo</p>
<p class="mt-1 font-display text-2xl font-bold text-sand">{{ fmtUsd(pnl.monthlyProfit) }}</p>
</div>
<div class="rounded-2xl border border-amber-400/40 bg-amber-400/[0.07] p-4">
<p class="text-xs uppercase tracking-wider text-amber-300">Profit / yr</p>
<p class="mt-1 font-display text-2xl font-bold text-sand">{{ fmtUsd(pnl.yearlyProfit) }}</p>
</div>
</div>
<div class="mt-6 grid items-start gap-8 lg:grid-cols-[1fr_1fr]">
<!-- revenue mix bar + live detail -->
<div>
<p class="mb-2 text-xs uppercase tracking-wider text-sand-dim">Revenue mix</p>
<div class="flex h-4 w-full overflow-hidden rounded-full">
<button
v-for="g in revenueGroups"
:key="g.id"
class="h-full cursor-pointer transition-opacity"
:class="[barBg[g.accent], selected && selected !== g.id ? 'opacity-30' : 'opacity-100']"
:style="{ width: `${revPct(g.id)}%` }"
:title="`${g.title} · ${fmtUsd(groupTotal(g))}`"
@click="toggle(g.id)"
/>
</div>
<Transition name="fade" mode="out-in">
<div
v-if="selectedGroup"
:key="selectedGroup.id"
class="mt-5 rounded-2xl border p-5"
:class="ring[selectedGroup.accent]"
>
<div class="flex items-center gap-2">
<Icon :name="selectedGroup.icon" class="size-5" :class="text[selectedGroup.accent]" />
<h3 class="font-display text-base font-semibold text-sand">{{ selectedGroup.title }}</h3>
<span class="ml-auto font-display text-lg font-semibold" :class="text[selectedGroup.accent]">
{{ fmtUsd(groupTotal(selectedGroup)) }}
</span>
</div>
<ul class="mt-3 space-y-2">
<li
v-for="item in selectedGroup.items"
:key="item.label"
class="flex items-baseline justify-between gap-3 text-sm"
>
<span class="text-sand-dim">
{{ item.label }}
<span v-if="item.note" class="text-[11px] text-sand-dim/60">· {{ item.note }}</span>
</span>
<span class="shrink-0 tabular-nums text-sand">{{ fmtUsd(item.amount) }}</span>
</li>
</ul>
</div>
<div
v-else
key="hint"
class="mt-5 flex items-center gap-3 rounded-2xl border border-dashed border-white/15 p-5 text-sm text-sand-dim"
>
<Icon name="ArrowRight" class="size-4 shrink-0 text-amber-300" />
Tap a category to see its line items.
</div>
</Transition>
</div>
<!-- clickable category list -->
<div class="space-y-2.5">
<button
v-for="g in pnlGroups"
:key="g.id"
class="flex w-full items-center gap-4 rounded-2xl border p-4 text-left transition"
:class="
selected === g.id
? ring[g.accent]
: 'border-white/10 bg-white/[0.03] hover:border-white/25 hover:bg-white/[0.06]'
"
@click="toggle(g.id)"
>
<span class="grid size-10 shrink-0 place-items-center rounded-xl bg-white/5" :class="text[g.accent]">
<Icon :name="g.icon" class="size-5" />
</span>
<div class="min-w-0 flex-1">
<h3 class="font-display text-base font-semibold text-sand">{{ g.title }}</h3>
<p class="text-xs uppercase tracking-wider text-sand-dim">{{ g.kind }}</p>
</div>
<span class="shrink-0 font-display text-lg font-semibold" :class="text[g.accent]">
{{ fmtUsd(groupTotal(g)) }}
</span>
<Icon name="ChevronRight" class="size-4 shrink-0 text-sand-dim transition-transform" :class="selected === g.id ? 'rotate-90' : ''" />
</button>
</div>
</div>
<p class="mt-4 text-[11px] text-sand-dim/70">
Figures recomputed from the plan's line items. The source summary stated
{{ fmtUsd(pnlDocSummary.revenue) }} rev / {{ fmtUsd(pnlDocSummary.expenses) }} exp
being reconciled. Lunarpunk Locals count is TBD.
</p>
</SlideFrame>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(6px);
}
</style>

View file

@ -1,33 +0,0 @@
<script setup lang="ts">
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import { revenueStreams } from '../data'
</script>
<template>
<SlideFrame
kicker="Not a cost centre — a business"
title="The nodes are fully operational event spaces"
subtitle="Every entity earns while it demonstrates the technology. Six revenue streams across the collaboration."
>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="r in revenueStreams"
:key="r.title"
class="flex items-start gap-4 rounded-2xl border border-white/10 bg-white/[0.03] p-5"
>
<span class="grid size-11 shrink-0 place-items-center rounded-xl bg-amber-400/10 text-amber-300">
<Icon :name="r.icon" class="size-5" />
</span>
<div>
<h3 class="font-display text-lg font-semibold text-sand">{{ r.title }}</h3>
<p class="mt-1 text-sm text-sand-dim">{{ r.note }}</p>
</div>
</div>
</div>
<p class="mt-8 text-sm text-sand-dim">
Investors get access to these spaces stays, retreats and residencies included in angel tiers.
</p>
</SlideFrame>
</template>

View file

@ -0,0 +1,41 @@
<script setup lang="ts">
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import { buildings } from '../data'
const icons = ['Building2', 'BedDouble', 'Cpu']
</script>
<template>
<SlideFrame
kicker="SolLunar Society · the building"
title="One address, three buildings"
subtitle="A members' society up front, two Yellow Houses behind — lodging, coworking and the data center that runs it all."
>
<div class="grid gap-4 md:grid-cols-3">
<div
v-for="(b, i) in buildings"
:key="b.name"
class="flex flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-5"
>
<div class="flex items-center gap-3">
<span class="grid size-10 place-items-center rounded-xl bg-indigo-500/10 text-indigo-300">
<Icon :name="icons[i] ?? 'Building2'" class="size-5" />
</span>
<h3 class="font-display text-base font-semibold leading-tight text-sand">{{ b.name }}</h3>
</div>
<ul class="mt-4 space-y-1.5">
<li v-for="r in b.rooms" :key="r" class="flex items-center gap-2 text-sm text-sand-dim">
<span class="size-1.5 rounded-full bg-indigo-400" />
{{ r }}
</li>
</ul>
</div>
</div>
<p class="mt-6 text-sm text-sand-dim">
The Yellow House data center makes SolLunar self-hosting the Archipelago stack runs on-site,
not in someone else's cloud.
</p>
</SlideFrame>
</template>

View file

@ -0,0 +1,32 @@
<script setup lang="ts">
import SlideFrame from '../components/SlideFrame.vue'
import Icon from '../components/Icon.vue'
import { tenants } from '../data'
</script>
<template>
<SlideFrame
kicker="SolLunar Society · tenants"
title="Five founding tenants"
subtitle="The hub is anchored by businesses that are also the community — each one a reason to become a member."
>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="t in tenants"
:key="t.name"
class="flex flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-5 transition hover:border-indigo-500/40 hover:bg-indigo-500/5"
>
<span class="grid size-10 place-items-center rounded-xl bg-indigo-500/10 text-indigo-300">
<Icon :name="t.icon" class="size-5" />
</span>
<h3 class="mt-4 font-display text-lg font-semibold text-sand">{{ t.name }}</h3>
<p class="mt-1.5 text-sm leading-relaxed text-sand-dim">{{ t.blurb }}</p>
</div>
</div>
<p class="mt-6 text-sm text-sand-dim">
Tenants pay rent <span class="text-sand">and</span> drive events, retail and food traffic
the same footfall that fills memberships.
</p>
</SlideFrame>
</template>

View file

@ -35,7 +35,12 @@ import { brand, surfaceEntities, accentText } from '../data'
{{ brand.subtagline }}
</p>
<p class="mt-12 text-xs text-sand-dim">
<span class="mt-6 inline-flex items-center gap-2 rounded-full border border-indigo-500/40 bg-indigo-500/10 px-4 py-1.5 text-xs font-medium text-indigo-300">
<span class="size-1.5 rounded-full bg-indigo-400" />
Raising now · {{ brand.firstNode }}
</span>
<p class="mt-10 text-xs text-sand-dim">
Use <kbd class="rounded border border-white/15 px-1.5 py-0.5"></kbd> /
<kbd class="rounded border border-white/15 px-1.5 py-0.5"></kbd> to navigate
</p>