Add unlisted /socialism pages for the worker-ownership model

Renders the markdown documents in Content/Socialism: a landing overview at
/socialism and chapters at /socialism/{slug}, with a sectioned sidebar,
on-page table of contents, prev/next paging and cross-document links.
Pages are noindex and nothing on the site links to them. Underscore-prefixed
files (founding draft, progress log) are never served, deployed or committed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-23 03:10:35 -06:00
co-authored by Claude Opus 5.5
parent 3326557043
commit e8f930e9fc
29 changed files with 1431 additions and 0 deletions
+3
View File
@@ -369,3 +369,6 @@ FodyWeavers.xsd
native/**/build/ native/**/build/
# Playwright MCP scratch output # Playwright MCP scratch output
.playwright-mcp/ .playwright-mcp/
# Private /socialism working files (founding draft, progress log) - repo is public
JoshHeaps.Net/Content/Socialism/_*.md
@@ -0,0 +1,22 @@
---
title: Capital, After
section: Capital and Growth
order: 50
summary: Outside investment still exists, but it's rented rather than owned: profit-sharing claims that expire and carry no vote.
---
# Capital, After
Outside investment doesn't disappear. It changes shape.
**Rented, not enthroned.** An investor funds the company and receives repayment plus a share of profits for the life of the loan. Then the claim **extinguishes**, and the workers hold 100%.
This is the elegant part and it should be said plainly: today's equity is *perpetual* — an original investor's great-grandchildren collect forever off work they never did. This claim *expires*. That's the whole reform, in one word. Capital gets rented. (And note the symmetry: the conversion itself now runs on this same instrument. The model has exactly one kind of outside capital claim, whether it's a VC funding a startup or a founder being bought out. One rule, no exceptions.)
The returns can be enormous. A profit-share loan into a risky seed-stage company might pay 10x or 30x. That's fine. The principle was never *no big payday* — it was **no perpetual claim and no control**.
**Be honest about what this is.** A profit-contingent return means the investor eats the loss when things go badly. That is residual risk, which is the economic job we call equity, whatever we print on the paper. Don't claim it "isn't a stake in the work" — it is. Claim what's actually true and actually better: *finite, and voteless.*
**The consequence, priced in.** Capital charges for risk. An instrument with equity-like downside and a terminating upside gets funded last, or demands a bigger profit share. Revenue-based financing already exists in today's market and stays niche for exactly this reason. This bites hardest where losses are likeliest — brand-new ventures with no track record and nothing to repossess. It barely bites at all on mature, profitable firms like Conservice.
**So the cost-of-capital problem is a startup problem, not a going-concern problem.** That narrows the open question from a vague abyss to a single well-defined one — and the [federation bank](federation.md) now stands inside the answer.
@@ -0,0 +1,25 @@
---
title: The Constitution
section: Guardrails
order: 90
summary: The power this model needs must be constrained: money out of politics, counter-majoritarian rights, and overlapping checks.
---
# The Constitution
Because every mechanism in this model runs through a state powerful enough to trace ownership, tax at 1950s rates, void untraceable claims, and set lookback dates (see [The Conversion](conversion.md)). That is a lot of concentrated power, currently aimed at a good target. **The design question is what stops the next person from aiming it at a bad one.**
**Money out of politics.** Legislative work pays well enough that good people want the job — but any money, gift, or benefit from a non-government entity ends the salary and starts a prison sentence. Politicians should benefit only from serving their people.
**But representation alone is not a guardrail.** This is the trapdoor, and it needs saying explicitly:
> *A government that perfectly represents its people will target a group whenever the people want that group targeted.*
Representation amplifies the majority; it does not restrain it. Jim Crow was representative — it represented its white voters faithfully, and that is exactly how it ground people down for a century. Internment was popular. Any mechanism that pays politicians based on how much their constituents liked their votes would have *paid a bonus* to the segregationist and docked the man who defended his Black constituents. Clean elections stop capture *by the rich*. They do nothing to stop the majority deciding a minority is the enemy.
**So: counter-majoritarian rights.** Things a person keeps *even when everyone around them votes to strip them away.* The whole point of a right is that it holds on the day the majority is wrong — and you cannot assume in advance that you'll always be the majority, or always be right. The guardrail is not there to protect the owning class from me. It's there to protect me from whoever inherits this machinery next.
- Keep the Bill of Rights. Make rights that protect individuals **extremely difficult to repeal.**
- **Two tiers.** Speech, due process, equal protection, freedom from imprisonment without trial: near-absolute. Property: strong, but balanceable against the public interest with compensation — which is what the Takings Clause already says, and always has. Property was never on the sacred tier. *(And the conversion now actually complies with this: full compensation, then taxation — the same two steps any government action must survive. The model no longer asks for an exception to its own constitution.)*
- **The Senate had the right idea, badly built.** Keep the cooling-saucer function — a second chamber that slows things down and forces broad coalitions instead of bare-51% steamrolls. Drop the geographic weighting. Land should not get votes. Equal state representation and the protection of slavery were the *same bargain* in 1787, and that structure later shielded Jim Crow. Over-weighting low-population states protects *whoever lives there* — an arbitrary category — not the vulnerable.
- **No final guardrail exists.** Entrench rights and you've moved power to whoever *interprets* them. There's no rule so perfect it guards itself. The answer isn't a better final rule — it's **redundancy**: several overlapping checks, none supreme, so that capturing any one of them doesn't hand someone the whole apparatus.
@@ -0,0 +1,45 @@
---
title: The Conversion
section: The Transition
order: 40
summary: How today's companies become worker-owned: a full-price buyout paid from future profits, with a steeply progressive tax on the proceeds.
---
# The Conversion
**A mandatory, one-time, full-price buyout — paid for by the whales themselves.**
The first draft capped compensation at a 1% stake and called it financing. Be honest: striking 99% of the debt is a seizure with paperwork, and it contradicted this document's own constitution, which keeps the Takings Clause — property balanceable against the public interest *with compensation*. You don't get to write "with compensation" in one section and delete it in another. So the cap is gone. Here is what replaces it, and it is stronger:
## The buyout, step by step
**1. Workers buy the firm at full fair value, financed out of its own future profits.** This is a leveraged buyout where the workers are the sponsor — the exact machine private equity used to buy Conservice, pointed the other way. Workers own and govern from day one; the purchase debt is serviced from the surplus that was leaving anyway.
**2. The buyout instrument is the same instrument investors get** (see [Capital, After](capital.md)): an expiring, profit-contingent, voteless claim. The exiting owner is paid from profits as profits arrive. No fixed debt schedule, so a recession mid-payback flexes the payments instead of bankrupting the firm. The owner bears residual risk during the payout — which is symmetric, principled, and exactly the deal every other capital provider gets under this model.
**3. A steeply progressive tax on the proceeds, traced to ultimate individuals.** This is where the moral weight lands, and it lands legally. A compensation cap on a forced sale dies in court as a taking. A tax on capital-sale proceeds is just a tax — the United States ran 91% top marginal rates in the 1950s and the sky held. The founder is paid the full number on paper; most of it, above the top brackets, comes home through a channel courts have blessed for a century.
**4. The tax proceeds buy down conversion debt across the economy.** The revenue doesn't vanish into the general fund — it is recycled as debt relief for converting firms, funds the [retirement bridge](#the-retirement-bridge), and funds [the floor](the-floor.md). **The whales at the top of the ownership chain finance everyone else's freedom.** That is the sentence that replaces "six weeks."
The result: the exiting owner walks away rich — rich in *consumption*, not in structural power over ten thousand people. Which was always the actual principle. This model never promised the founder no payday. It promised **no perpetual claim and no control**, and now it applies that rule to everyone who holds capital, including the people cashing out on day one.
## The machinery
The machinery survives from the first draft, repurposed:
- **Ownership is traced through to ultimate individuals.** TPG is owned by someone; those owners are owned by someone. The circular ownership is solvable with matrix algebra, and it has already been done — a 2011 ETH Zurich study traced 43,000 companies through exactly this tangle. Tracing now serves the tax brackets instead of a cap.
- **This is why pensions survive.** Look *through* the index fund and the retiree holds a millionth of a percent — her proceeds land in the bottom bracket and she keeps nearly all of them. The firefighter keeps his retirement; the founder pays 1950s rates. Same rule.
- **Cracking the shells.** Any ownership claim that cannot be traced to a named human is **legally unenforceable in US courts**. Hidden owners can't collect, can't sue, can't foreclose. Transparency becomes the profitable choice. Precedent: FATCA told foreign banks "report your American account holders or eat a punishing tax," and they complied.
- **Killing the fire-sale window.** A lookback date — ownership as of the day the bill is introduced is what counts. Transfers, restructurings, and paper losses after that date don't dodge the brackets.
## Valuation: settled by rule, not by litigation
The first draft flagged this as a hole; full-price conversion makes it *the* hole, so it gets machinery:
- **Public firms:** market price on the lookback date. No appraisal, no argument — the market already voted.
- **Recently traded private firms:** the last arm's-length transaction price. Conservice converts at the $5 billion TPG actually paid. There is a special justice in the owner being held to the number they chose.
- **Everything else:** a statutory formula — a sector-standard multiple of trailing multi-year average profit. Either side may demand a formal appraisal, but the formula anchors it and the loser pays costs. The goal is that appraisal is the exception, not the economy.
## The retirement bridge
Conversion is unjust to whoever happens to be 63 when it lands — they lose the windfall the 30-year-old will ride for decades. So: supplemental retirement funding for 15–20 years, tapering as profit-sharing income accumulates — now with a named funding source: the conversion tax.
@@ -0,0 +1,20 @@
---
title: The Federation
section: The Firm
order: 30
summary: Firms pool risk through a shared solidarity fund and internal bank, so one company failing doesn't mean your life failing.
---
# The Federation
This section exists because of a problem the first draft never named: **concentration.**
A worker-owner holds their salary, their profit share, and their skills all inside one firm. That is exactly the risk a shareholder diversifies away and a worker cannot. And it is the real source of risk-aversion — if one bad bet can kill the company that is also your paycheck and your equity, you take the safe option every time, and an economy of safe options stops producing breakthroughs.
The fix is not a rule inside the firm. It is a structure between firms, and Mondragon already built it:
- **Firms pool a slice of profits into a mutual solidarity fund.** The fund absorbs shocks — a firm having a bad year draws support instead of dying.
- **When a firm does die, the federation redeploys its workers into member firms.** Your company failing stops meaning your life failing. *This* is what licenses healthy risk-taking — not lower stakes on paper, but a floor under the fall.
- **The fund doubles as an internal bank** (Mondragon's Caja Laboral is the working precedent), lending to member firms and seeding new ones on the [expiring profit-share terms](capital.md). The federation becomes a native source of the patient capital the model otherwise struggles to attract.
Membership is voluntary but the incentives all point in: unfederated firms face the concentration problem alone.
+36
View File
@@ -0,0 +1,36 @@
---
title: Free-Market Democratic Socialism
summary: A working model for an economy where the people who do the work own and govern the companies they work for, while markets, wages, investment and personal property all stay.
---
# Free-Market Democratic Socialism
## The idea in one sentence
**Your house, car, savings and tools are yours, and nobody touches them. But a permanent, tradeable claim on the work of thousands of strangers is a different kind of thing, and it shouldn't be treated as the same "property."** Everything else in this model follows from that line. [Read the thesis](thesis.md).
## What changes
- **Workers own their company.** Every employee gets one vote from day one. A share of the profits grows with tenure until everyone who's been there long enough splits them equally. [Worker Ownership](worker-ownership.md)
- **Companies back each other up.** Worker-owned firms pool part of their profits into a shared fund and bank. If one firm fails, its workers are moved into others instead of being left with nothing. [The Federation](federation.md)
- **Existing companies are bought, not seized.** Workers buy their company at full price and pay for it out of its future profits. A steeply progressive tax on the sale proceeds then funds the transition for everyone else. [The Conversion](conversion.md)
- **Investment is rented, not owned forever.** Investors still fund companies and can still earn huge returns, but their claim runs out and comes with no vote. [Capital, After](capital.md)
- **Nobody is left behind by low-margin work.** Ownership doesn't help much when there's barely any profit to own, so a strong floor covers the rest. [Ownership and the Floor](the-floor.md)
## What stays the same
| Stays | Why |
| --- | --- |
| Markets and competition | Companies still compete, set prices and win or lose customers. |
| Salaries | Hard and in-demand jobs still pay more. A surgeon still earns a surgeon's salary. |
| Personal property | Homes, cars, savings and tools are untouchable. |
| Investment and big returns | Investors can still make 10x or 30x. They just can't own a company forever. |
| Rights and due process | The model makes individual rights harder to repeal. [The Constitution](constitution.md) |
## Why it matters
This isn't mainly about money. **People deserve a say in governing the place where they spend most of their waking lives.** Today most workers are ruled by distant owners who answer to nobody who works there. The model ends that rule without taking anyone's personal wealth. [Making the Case](the-argument.md)
## Honest about what's unsolved
This is a working model, not a finished one. It is being stress-tested against critics from across the political spectrum, and the hard problems it hasn't cracked yet are listed in the open. [Open Problems](open-problems.md)
@@ -0,0 +1,28 @@
---
title: Open Problems
section: Guardrails
order: 100
summary: An honest list of what isn't solved yet.
---
# Open Problems
Honest list, updated. Several first-draft holes are patched above; here is what remains, and what's new.
1. **Valuation at scale.** Now the weakest link. The formula-first machinery (market price / last transaction / statutory multiple) turns most conversions into arithmetic, but the residual appraisal fights on large, never-traded private firms are exactly the fights with the most money and the best lawyers on the other side. Formula anchoring and loser-pays shrink this; nothing eliminates it.
2. **The anticipation window.** *(Mostly patched — one true residual.)* The lookback closes almost everything: ownership and price are fixed as of the day the bill is introduced, so post-introduction transfers, restructurings, and paper losses change nothing. Value-stripping between introduction and closing — the owner paying himself a giant special dividend while still nominally in charge — is closed with a mechanism private equity already uses in its own deals: the **locked box**. Price is set at the lookback date, and any leakage after it (extraordinary dividends, above-market salary, related-party debt) is deducted straight from the purchase price. What remains is the one thing no law can pre-date: extraction and emigration *before the bill is introduced*, while the movement is visibly gaining steam. Expatriation-triggered mark-to-market tax (§877A, already in US law) catches part of that; a well-advised owner with a two-year head start still finds seams. That window is the honest residual — and it shrinks the faster the bill moves from idea to introduction.
3. **Foreign capital.** Softened but not solved. Norway's sovereign wealth fund and Japanese pensions now get *paid in full* and taxed like everyone else, which is a much easier diplomatic sell than a 99% haircut. But the flows question remains: does new foreign money keep arriving in an economy where equity expires? "We have enough resources" doesn't answer it. The honest answer is that expiring capital pays a premium, and the premium is the price of the structure.
4. **The payout decade.** Full price means large firms spend years paying out their former owners. The profit-contingent instrument removes the bankruptcy risk, but for that period a big slice of the surplus still flows to the old owner — workers govern immediately but get rich slowly. The conversion-tax recycling shortens this; it doesn't erase it. Anyone promised otherwise was promised a seizure.
5. **Growth and dilution.** Softened by the tenure ramp, not eliminated. In steady state, an equal split per head still means a marginal hire must generate the average surplus to break even for incumbents. Capital-intensive expansion into lower-margin lines will face internal resistance conventional firms don't have.
6. **Who decides which sectors are "essential"?** Subsidy authority is price-setting power over a large chunk of the labor market, and every low-margin industry will suddenly discover it is Essential. Same concentration-of-power problem as the conversion apparatus, wearing an apron.
7. **The contractor boundary.** *(Partially solved.)* The fix: a cleaning company's workers own the cleaning company — no unowned underclass, just a chain of owned firms selling services to each other. Independent contracting is heavily restricted; companies cannot spin up a captive contractor firm to hire from. Worker-owned staffing agencies double as de facto unions. **But read California's AB5 first** — it tried nearly this bright-line test in 2019, some firms reclassified, some cut workers loose, unrelated freelancers got crushed, and Uber and Lyft spent $200M on Prop 22 to carve themselves out and won. That's the case study for where the pressure lands and who fights back.
8. **The 1/N problem.** *(Mostly answered.)* Peer monitoring works at 30 people; at 10,000 your extra effort raises the pool by a rounding error. The answer is culture and structure — coops really do push out free riders, and the federation model keeps units small enough for monitoring to bite while pooling risk above them.
*Patched since the first draft:* the taking (full price + progressive tax), "nobody sells again" (conversion is mandatory and universal, not sale-triggered — there is no dodge-by-holding), the fire-sale window (lookback), risk concentration ([the federation](federation.md)), the hiring disincentive ([the tenure ramp](worker-ownership.md)), and subsidy capture on the worker floor (pay workers directly).
@@ -0,0 +1,20 @@
---
title: Prison Labor
section: Society
order: 80
summary: Only with full pay parity, and only if no budget ever benefits from more people being locked up.
---
# Prison Labor
Incarcerated people work **the same jobs, at the same pay, side by side with free temps.**
Pay parity is not a detail. It is the entire moral load-bearing wall, and it is the first thing a critic will assume was left out — so it goes in the first sentence, always.
Pay parity is what separates this from convict leasing. The historical horror ran on a *cost advantage*: after the Civil War, Southern prisons became labor suppliers and arrests rose to meet demand. The Thirteenth Amendment abolished slavery *"except as a punishment for crime,"* and that clause is why it worked, and it is still in the Constitution. Remove the cost advantage and the state gains nothing by locking more people up.
**The test, applied forever:** *if automation arrived tomorrow and every prison job vanished, would any budget line get harder to balance?* If yes, the trap is live and the design has failed.
Required guardrails: pay parity, genuinely optional participation, no quotas, no budget line that improves with a larger prison population.
**The goal is reform, not punishment.** People should leave prison with savings, a work history, and a reference. Employment after release is one of the strongest predictors of not going back.
@@ -0,0 +1,19 @@
---
title: Startups
section: Capital and Growth
order: 60
summary: Fund the science, backstop the boring, seed the funders, and don't let government pick the startups.
---
# Startups
**One line: fund the science, backstop the boring, seed the funders — don't pick the startups.**
Government should not be a venture capitalist. VC works by being wrong 70% of the time and letting two freak winners pay for everything. No public agency survives that headline rate — it would get cautious, fund safe bets and well-connected people, and safe bets don't produce breakthroughs. The failure mode isn't corruption; it's risk-aversion under political accountability. A central allocator also has *one* thesis and one set of blind spots, and when it's wrong it's wrong across the whole portfolio at once.
Four moves instead:
1. **Fund the science, not the companies.** DARPA and NIH gave us the internet, GPS, touchscreens, mRNA. The state buys the ingredients; a thousand private actors race to cook.
2. **Seed the funders, not the founders.** Don't build one federal VC. Put public money into many independent funds with different theses and let them compete. Israel's Yozma program did this and bootstrapped one of the densest startup ecosystems on earth.
3. **Startups start worker-owned by default.** A garage company is founders and early employees — almost no separation between labor and ownership. Let them own it outright and let outside money in on expiring profit-share loans. Be honest about the cost: this is exactly where the expiring instrument prices worst, so early-stage capital will be more expensive than the perpetual-equity world it replaces. The Yozma-style funds and the federation bank exist to close that gap on purpose, with public and mutual money that accepts a thinner return for the structure.
4. **Backstop the boring.** Rural grocery store, local clinic, unglamorous manufacturer. That's development banking, not venture, and government is genuinely good at it. Loan guarantees, SBA-style.
@@ -0,0 +1,28 @@
---
title: Making the Case
section: Making the Case
order: 110
summary: Lead with Germany and the leveraged buyout, and argue from freedom, not dollars.
---
# Making the Case
**Lead with Germany, not OSHA.** Co-determination puts workers on up to half the supervisory board of large firms, by law, non-optionally — and Germany is still a capitalist market economy with a stock exchange. "You can't force ownership changes and stay a free market" is empirically false and has been for fifty years.
**But know the difference.** German co-determination reallocates *governance* (board seats) while leaving *cash-flow ownership* untouched. Investors kept every euro. That's why capital tolerated it — it was cheap to concede because it didn't touch the money. This model *does* touch the money. It is not a slight tweak on the German model; it is the German model with a different engine.
**And now the second precedent: the leveraged buyout.** Workers buying their firm at full price out of its own cash flows is not exotic finance — it is the exact transaction private equity performs thousands of times a year. The model doesn't abolish the LBO. It changes who the sponsor is. That framing is legible to every finance professional in the country, and it is true.
**Don't lead with "they extract while workers starve."** It's the weakest version of the case, and it invites "the return is compensation for capital and risk," which wins the exchange. The strong version:
> *Bargaining power is lopsided. Workers have no realistic path to a claim on the enterprise they build. The return to capital is wildly disproportionate to actual risk once a firm is mature. And the market for corporate ownership is rigged to keep it that way.*
That's a fight worth having and it can be won.
**Don't argue it in dollars.** The dollars framing ($150k/head, hungry kids) is exactly what leaves the model vulnerable to *"then just give them cash — a big enough UBI is simpler."* If ownership is only an *instrument* for moving money, a UBI substitutes for it.
It isn't. It's the goal. **People deserve to govern the thing they spend their life inside.** The objection is *domination*, not just poverty — being ruled by a distant firm that answers to nobody who works there. That claim is about human freedom, and it survives the UBI, and no amount of cash dissolves it.
And notice the conversion redesign makes this cleaner, not muddier: the founder still gets paid, still gets rich, and the workers *still* need the model — because the model was never about taking his money. It was about ending his rule.
Say that, loudly, and the model can't be argued away.
@@ -0,0 +1,32 @@
---
title: Ownership and the Floor
section: Society
order: 70
summary: Ownership ends the skim, but a floor ends the hunger. Low-margin work needs both.
---
# Ownership and the Floor
This is the correction that matters most, and it took the longest to see.
**Equal ownership within firms does not produce equal outcomes across them.**
A worker-owned cleaning company still operates on cleaning-company margins — maybe $8k of profit per head. A worker-owned software firm throws off $150k per head. Both workers are owners. One of them still can't feed their kids.
Profit-per-head is set by technology and demand elasticity — forces ownership rules don't reach. Cleaning productivity has barely moved in forty years: one person, one mop. Ownership of the surplus doesn't change how much surplus *exists*.
So:
- **Ownership ends the skim.** It is the right answer to *extraction* — a large surplus flowing to people who didn't make it. It fixes the Conservice case completely.
- **A floor ends the hunger.** The home health aide, the childcare worker, the line cook — their problem was never that private equity was skimming their $150k. Their problem is there is no $150k.
You need both. They do different jobs. **One in six Utah kids goes hungry, and a lot of their parents work in sectors where there is barely any surplus to own.**
The floor, in pieces:
- A strong minimum wage.
- **Targeted subsidy for essential low-margin work — paid to workers, not firms.** Somebody has to clean the nursing home for the poor. If the sector can't pay, the public does, directly, so the buyer can't absorb the subsidy by bidding less.
- **Funded automation research aimed at exactly those sectors.** If productivity can't rise on its own, raise it. This is explicitly a *transition* strategy — support the workers until the technology lands. Say that out loud, or you end up permanently subsidizing a job you're simultaneously trying to eliminate, and the subsidized workers start lobbying against the automation. (This is what happened with coal.)
- **A transitional public temp corps.** Companies apply for subsidized labor. The state supplies temps and funds the automation in parallel. Everyone knows going in that the job has a sunset. Voluntary, real wages, open to anyone who wants a bridge — which costs almost nothing on the margin, since these people are already covered by the floor.
**The endpoint:** a society where work is not the price of being allowed to eat. Where people who don't work are taken care of, and people who work are people who want *more* — the finer things, a bigger share, the reward for doing what's hard or what nobody else will do.
+18
View File
@@ -0,0 +1,18 @@
---
title: The Thesis
section: Foundations
order: 10
summary: Personal property is untouchable. Productive capital, a perpetual claim on other people's labor, is a different thing.
---
# The Thesis
**The word "property" protects two very different things, and only one of them deserves that protection.**
There is *personal property* — your house, your car, your savings, your tools. The things you live in and use. These are untouchable. Nobody is collectivizing your couch.
Then there is *productive capital* — a perpetual, tradeable claim on the output of a thousand people you will never meet. This is not the same kind of right. It has been wearing the same word by accident, and the accident does political work for the people who benefit from it.
The dividing line: **it stops being a tool and becomes capital when it earns you a claim on other people's labor that you don't work alongside.** The plumber's van is not extracting from anyone. TPG's stake in Conservice is.
Everything below follows from that sentence. If it holds, the rest holds. If it doesn't, none of it does.
@@ -0,0 +1,23 @@
---
title: Worker Ownership
section: The Firm
order: 20
summary: Every employee owns their company: one worker, one vote from day one, and a profit share that grows with tenure and then caps.
---
# Worker Ownership
Every employee is an owner of the company they work for. That means two things, and they are not the same thing:
**One worker, one vote. Unconditional, from day one.** This is the moral core and it does not ramp, vest, or vary. The objection to the current system is *domination* — being ruled by a distant firm that answers to nobody who works there. The cure for domination is the vote, and you don't earn your way into not being dominated.
**Profit share ramps with tenure, then caps.** A new hire starts at a partial share and grows to a full, equal share over roughly five to ten years. After the cap, everyone's share is identical — the thirty-year lifer and the eight-year veteran split profit the same.
Why the ramp: it kills the dilution panic. Under flat equal shares, every new hire immediately eats everyone's slice, and cooperatives empirically under-hire for exactly that reason. With a ramp, hiring decisions move toward "does this person grow the pie" during precisely the window when their value is most uncertain.
Why the cap: uncapped tenure weighting builds a gerontocracy of shares and chains people to their first employer. Capped at a career-fraction, people can change companies, move cities, start over — and still reach a full share with plenty of career left.
- **Base salaries still exist**, set by the company. A brain surgeon won't work somewhere without a high salary, and shouldn't have to. Hard jobs and in-demand jobs still pay more. The incentive to do difficult work survives intact.
- **Governance is up to each company.** Workers pass bylaws deciding what requires a vote and what doesn't. Some will use direct democracy, some representative systems, some will delegate almost everything to management they hire and can fire. "Workers vote on every decision" is a strawman and always was.
The concrete case: the average employee where I work generates roughly $150,000/year in profit after all costs. That money currently flows upward to private equity. Under this model it flows to the people who made it.
+9
View File
@@ -9,9 +9,18 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Markdig" Version="1.4.0" />
<PackageReference Include="System.Text.Json" Version="9.0.3" /> <PackageReference Include="System.Text.Json" Version="9.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<!-- Public /socialism documents. Underscore-prefixed files (founding draft, progress log) are private and never deployed. -->
<Content Include="Content/Socialism/*.md" Exclude="Content/Socialism/_*.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="Resources/**"> <Content Include="Resources/**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+15
View File
@@ -0,0 +1,15 @@
namespace JoshHeaps.Net.Models;
public class SocialismDoc
{
public string Slug { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Summary { get; set; } = string.Empty;
public string Section { get; set; } = string.Empty;
public int Order { get; set; }
public string HtmlContent { get; set; } = string.Empty;
public List<SocialismHeading> Headings { get; set; } = [];
public int ReadingMinutes { get; set; }
}
public record SocialismHeading(int Level, string Id, string Text);
+60
View File
@@ -0,0 +1,60 @@
@page "/socialism/{slug}"
@model JoshHeaps.Net.Pages.Socialism.DocModel
@{
ViewData["Title"] = Model.Doc!.Title + " - JoshHeaps.Net";
Layout = "_Layout";
}
<div class="soc-progress" id="socProgress"></div>
<button class="soc-menu-toggle" id="socMenuToggle" aria-controls="socSidebar" aria-expanded="false">Contents</button>
<div class="soc-shell">
<partial name="_Nav" model="Model" />
<div class="soc-main">
<nav class="soc-breadcrumbs" aria-label="Breadcrumb">
<a href="/socialism">Overview</a>
@if (!string.IsNullOrEmpty(Model.Doc.Section))
{
<span>/</span><span>@Model.Doc.Section</span>
}
</nav>
<header class="soc-doc-header">
<h1 class="soc-doc-title">@Model.Doc.Title</h1>
@if (!string.IsNullOrEmpty(Model.Doc.Summary))
{
<p class="soc-doc-summary">@Model.Doc.Summary</p>
}
<span class="soc-doc-meta">@Model.Doc.ReadingMinutes min read</span>
</header>
<article class="soc-content">
@Html.Raw(Model.Doc.HtmlContent)
</article>
<nav class="soc-pager" aria-label="Previous and next">
@if (Model.Previous is not null)
{
<a class="soc-pager-link prev" href="@(Model.Previous.Slug == SocialismDocService.IndexSlug ? "/socialism" : "/socialism/" + Model.Previous.Slug)">
<span class="soc-pager-label">&larr; Previous</span>
<span class="soc-pager-title">@(Model.Previous.Slug == SocialismDocService.IndexSlug ? "Overview" : Model.Previous.Title)</span>
</a>
}
@if (Model.Next is not null)
{
<a class="soc-pager-link next" href="/socialism/@Model.Next.Slug">
<span class="soc-pager-label">Next &rarr;</span>
<span class="soc-pager-title">@Model.Next.Title</span>
</a>
}
</nav>
</div>
<partial name="_Toc" model="Model.Doc.Headings" />
</div>
@section Styles {
<partial name="_Head" />
}
@@ -0,0 +1,31 @@
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Implementations;
using JoshHeaps.Net.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace JoshHeaps.Net.Pages.Socialism;
public class DocModel(ISocialismDocService docService) : SocialismPageModel
{
public SocialismDoc? Doc { get; private set; }
public SocialismDoc? Previous { get; private set; }
public SocialismDoc? Next { get; private set; }
public IActionResult OnGet(string slug)
{
if (slug == SocialismDocService.IndexSlug)
return RedirectPermanent("/socialism");
Doc = docService.GetDoc(slug);
if (Doc is null)
return NotFound();
CurrentSlug = slug;
AllDocs = docService.GetAllDocs();
var position = AllDocs.FindIndex(d => d.Slug == slug);
Previous = position > 0 ? AllDocs[position - 1] : null;
Next = position >= 0 && position < AllDocs.Count - 1 ? AllDocs[position + 1] : null;
return Page();
}
}
@@ -0,0 +1,65 @@
@page "/socialism"
@model JoshHeaps.Net.Pages.Socialism.IndexModel
@{
ViewData["Title"] = Model.Overview!.Title + " - JoshHeaps.Net";
Layout = "_Layout";
}
<div class="soc-progress" id="socProgress"></div>
<button class="soc-menu-toggle" id="socMenuToggle" aria-controls="socSidebar" aria-expanded="false">Contents</button>
<div class="soc-shell">
<partial name="_Nav" model="Model" />
<div class="soc-main">
<header class="soc-hero">
<h1 class="soc-hero-title">@Model.Overview.Title</h1>
@if (!string.IsNullOrEmpty(Model.Overview.Summary))
{
<p class="soc-hero-summary">@Model.Overview.Summary</p>
}
@if (Model.Chapters.Count > 0)
{
<a class="soc-hero-cta" href="/socialism/@Model.Chapters[0].Slug">Start reading &rarr;</a>
}
</header>
<article class="soc-content">
@Html.Raw(Model.Overview.HtmlContent)
</article>
@if (Model.Chapters.Count > 0)
{
<section class="soc-chapters" id="explore">
<h2 class="soc-chapters-title">Explore the full model</h2>
@foreach (var group in Model.Chapters.GroupBy(d => d.Section))
{
@if (!string.IsNullOrEmpty(group.Key))
{
<h3 class="soc-chapters-group">@group.Key</h3>
}
<div class="soc-card-grid">
@foreach (var doc in group)
{
<a class="soc-card" href="/socialism/@doc.Slug">
<span class="soc-card-title">@doc.Title</span>
@if (!string.IsNullOrEmpty(doc.Summary))
{
<span class="soc-card-summary">@doc.Summary</span>
}
<span class="soc-card-meta">@doc.ReadingMinutes min read</span>
</a>
}
</div>
}
</section>
}
</div>
<partial name="_Toc" model="Model.Overview.Headings" />
</div>
@section Styles {
<partial name="_Head" />
}
@@ -0,0 +1,24 @@
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Implementations;
using JoshHeaps.Net.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace JoshHeaps.Net.Pages.Socialism;
public class IndexModel(ISocialismDocService docService) : SocialismPageModel
{
public SocialismDoc? Overview { get; private set; }
public List<SocialismDoc> Chapters { get; private set; } = [];
public IActionResult OnGet()
{
Overview = docService.GetDoc(SocialismDocService.IndexSlug);
if (Overview is null)
return NotFound();
CurrentSlug = SocialismDocService.IndexSlug;
AllDocs = docService.GetAllDocs();
Chapters = AllDocs.Where(d => d.Slug != SocialismDocService.IndexSlug).ToList();
return Page();
}
}
@@ -0,0 +1,21 @@
using JoshHeaps.Net.Models;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace JoshHeaps.Net.Pages.Socialism;
/// <summary>
/// Shared base for the /socialism pages. These pages are unlisted: nothing links to them
/// and search engines are told not to index them.
/// </summary>
public abstract class SocialismPageModel : PageModel
{
public List<SocialismDoc> AllDocs { get; protected set; } = [];
public string CurrentSlug { get; protected set; } = string.Empty;
public override void OnPageHandlerExecuting(PageHandlerExecutingContext context)
{
Response.Headers["X-Robots-Tag"] = "noindex, nofollow";
base.OnPageHandlerExecuting(context);
}
}
@@ -0,0 +1,4 @@
<meta name="robots" content="noindex, nofollow" />
<link href="/css/variables.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<link href="/css/socialism/socialism.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
<script src="/js/socialism/socialism.js?v=@ViewData["cssVersion"]" defer></script>
+32
View File
@@ -0,0 +1,32 @@
@model SocialismPageModel
@{
var groups = Model.AllDocs
.Where(d => d.Slug != SocialismDocService.IndexSlug)
.GroupBy(d => d.Section);
}
<nav class="soc-sidebar" id="socSidebar" aria-label="Document navigation">
<a href="/socialism" class="soc-sidebar-home @(Model.CurrentSlug == SocialismDocService.IndexSlug ? "active" : "")">Overview</a>
<input type="search" class="soc-filter" id="socFilter" placeholder="Filter sections..." aria-label="Filter sections" />
@foreach (var group in groups)
{
<div class="soc-nav-group">
@if (!string.IsNullOrEmpty(group.Key))
{
<div class="soc-nav-group-title">@group.Key</div>
}
<ul>
@foreach (var doc in group)
{
<li>
<a href="/socialism/@doc.Slug"
class="soc-nav-link @(doc.Slug == Model.CurrentSlug ? "active" : "")"
data-filter-text="@doc.Title.ToLowerInvariant() @doc.Summary.ToLowerInvariant()">@doc.Title</a>
</li>
}
</ul>
</div>
}
</nav>
+16
View File
@@ -0,0 +1,16 @@
@model List<SocialismHeading>
@if (Model.Count > 1)
{
<aside class="soc-toc" aria-label="On this page">
<div class="soc-toc-title">On this page</div>
<ul>
@foreach (var heading in Model)
{
<li class="[email protected]">
<a href="#@heading.Id" data-toc-id="@heading.Id">@heading.Text</a>
</li>
}
</ul>
</aside>
}
@@ -0,0 +1,4 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using JoshHeaps.Net.Models
@using JoshHeaps.Net.Pages.Socialism
@using JoshHeaps.Net.Services.Implementations
+1
View File
@@ -20,6 +20,7 @@ builder.Services.AddHttpClient("BlogApi", client =>
client.Timeout = TimeSpan.FromSeconds(10); client.Timeout = TimeSpan.FromSeconds(10);
}); });
builder.Services.AddSingleton<IBlogService, BlogService>(); builder.Services.AddSingleton<IBlogService, BlogService>();
builder.Services.AddSingleton<ISocialismDocService, SocialismDocService>();
builder.Services.AddSingleton<IChessService, ChessService>(); builder.Services.AddSingleton<IChessService, ChessService>();
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>(); builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
@@ -0,0 +1,178 @@
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using JoshHeaps.Net.Models;
using JoshHeaps.Net.Services.Interfaces;
using Markdig;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace JoshHeaps.Net.Services.Implementations;
/// <summary>
/// Serves the markdown documents in Content/Socialism. Files starting with an underscore
/// (the founding draft, the progress log) are private and never served.
/// </summary>
public partial class SocialismDocService : ISocialismDocService
{
public const string IndexSlug = "index";
private readonly string _contentDir;
private readonly ILogger<SocialismDocService> _logger;
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
public SocialismDocService(IWebHostEnvironment env, ILogger<SocialismDocService> logger)
{
_contentDir = Path.Combine(env.ContentRootPath, "Content", "Socialism");
_logger = logger;
}
public SocialismDoc? GetDoc(string slug)
{
if (!IsValidSlug(slug))
return null;
var path = Path.Combine(_contentDir, slug + ".md");
if (!File.Exists(path))
return null;
// Cache keyed on last-write time so edited documents show up without a restart.
var lastWrite = File.GetLastWriteTimeUtc(path);
if (_cache.TryGetValue(slug, out var entry) && entry.LastWrite == lastWrite)
return entry.Doc;
try
{
var doc = Parse(slug, File.ReadAllText(path));
_cache[slug] = new CacheEntry(doc, lastWrite);
return doc;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to render socialism doc {Slug}", slug);
return entry?.Doc;
}
}
public List<SocialismDoc> GetAllDocs()
{
if (!Directory.Exists(_contentDir))
return [];
return Directory.EnumerateFiles(_contentDir, "*.md")
.Select(Path.GetFileNameWithoutExtension)
.Where(slug => slug is not null && IsValidSlug(slug))
.Select(slug => GetDoc(slug!))
.OfType<SocialismDoc>()
.OrderBy(d => d.Slug == IndexSlug ? 0 : 1)
.ThenBy(d => d.Order)
.ThenBy(d => d.Title)
.ToList();
}
// Lowercase letters, digits and hyphens only: rules out private "_" files and path traversal.
private static bool IsValidSlug(string slug) => SlugRegex().IsMatch(slug);
private static SocialismDoc Parse(string slug, string raw)
{
var (meta, body) = SplitFrontMatter(raw);
var markdown = Markdown.Parse(body, Pipeline);
var headings = new List<SocialismHeading>();
string? firstH1 = null;
foreach (var heading in markdown.Descendants<HeadingBlock>())
{
var text = InlineText(heading.Inline);
if (heading.Level == 1)
{
firstH1 ??= text;
continue;
}
var id = heading.GetAttributes().Id;
if (heading.Level <= 3 && !string.IsNullOrEmpty(id))
headings.Add(new SocialismHeading(heading.Level, id, text));
}
foreach (var link in markdown.Descendants<LinkInline>())
{
if (!link.IsImage && link.Url is not null)
link.Url = RewriteLink(link.Url);
}
var words = WordRegex().Matches(body).Count;
return new SocialismDoc
{
Slug = slug,
Title = meta.GetValueOrDefault("title") ?? firstH1 ?? slug,
Summary = meta.GetValueOrDefault("summary") ?? string.Empty,
Section = meta.GetValueOrDefault("section") ?? string.Empty,
Order = int.TryParse(meta.GetValueOrDefault("order"), out var order) ? order : int.MaxValue,
HtmlContent = markdown.ToHtml(Pipeline),
Headings = headings,
ReadingMinutes = Math.Max(1, (int)Math.Round(words / 230.0))
};
}
// "economy.md#floor" -> "/socialism/economy#floor"; "index.md" -> "/socialism".
private static string RewriteLink(string url)
{
var match = RelativeDocLinkRegex().Match(url);
if (!match.Success)
return url;
var target = match.Groups["slug"].Value;
var anchor = match.Groups["anchor"].Value;
return (target == IndexSlug ? "/socialism" : $"/socialism/{target}") + anchor;
}
private static (Dictionary<string, string> Meta, string Body) SplitFrontMatter(string raw)
{
var meta = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var match = FrontMatterRegex().Match(raw);
if (!match.Success)
return (meta, raw);
foreach (var line in match.Groups["yaml"].Value.Split('\n'))
{
var idx = line.IndexOf(':');
if (idx <= 0)
continue;
meta[line[..idx].Trim()] = line[(idx + 1)..].Trim().Trim('"');
}
return (meta, raw[match.Length..]);
}
private static string InlineText(ContainerInline? inline)
{
if (inline is null)
return string.Empty;
return string.Concat(inline.Descendants<Inline>().Select(i => i switch
{
LiteralInline literal => literal.Content.ToString(),
CodeInline code => code.Content,
_ => string.Empty
}));
}
[GeneratedRegex("^[a-z0-9][a-z0-9-]*$")]
private static partial Regex SlugRegex();
[GeneratedRegex(@"^(?:\./)?(?<slug>[a-z0-9][a-z0-9-]*)\.md(?<anchor>#[^\s]*)?$")]
private static partial Regex RelativeDocLinkRegex();
[GeneratedRegex(@"\A---\r?\n(?<yaml>.*?)\r?\n---\r?\n", RegexOptions.Singleline)]
private static partial Regex FrontMatterRegex();
[GeneratedRegex(@"\b\w+\b")]
private static partial Regex WordRegex();
private record CacheEntry(SocialismDoc Doc, DateTime LastWrite);
}
@@ -0,0 +1,9 @@
using JoshHeaps.Net.Models;
namespace JoshHeaps.Net.Services.Interfaces;
public interface ISocialismDocService
{
SocialismDoc? GetDoc(string slug);
List<SocialismDoc> GetAllDocs();
}
@@ -0,0 +1,557 @@
*, *::before, *::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
scroll-padding-top: 1.5rem;
}
body {
margin: 0;
background-color: var(--color-bg);
color: var(--color-text);
font-family: var(--font-body);
line-height: 1.6;
}
/* ── Reading progress ── */
.soc-progress {
position: fixed;
top: 0;
left: 0;
height: 2px;
width: 0;
background: var(--color-accent);
z-index: 20;
transition: width 0.1s linear;
}
/* ── Three-column shell: nav | content | on-this-page ── */
.soc-shell {
display: grid;
grid-template-columns: 250px minmax(0, 760px) 220px;
justify-content: center;
gap: 3rem;
padding: 0 clamp(1rem, 3vw, 2rem);
}
.soc-main {
padding: 4rem 0 6rem;
min-width: 0;
}
/* ── Sidebar ── */
.soc-sidebar {
position: sticky;
top: 0;
align-self: start;
max-height: 100vh;
overflow-y: auto;
padding: 4rem 0 2rem;
scrollbar-width: thin;
scrollbar-color: var(--color-border) transparent;
}
.soc-sidebar-home {
display: block;
color: var(--color-heading-secondary);
font-weight: 600;
text-decoration: none;
margin-bottom: 1rem;
}
.soc-sidebar-home.active,
.soc-sidebar-home:hover {
color: var(--color-accent);
}
.soc-filter {
width: 100%;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 6px;
color: var(--color-heading-secondary);
padding: 0.45rem 0.65rem;
font: inherit;
font-size: 0.8rem;
margin-bottom: 1.25rem;
}
.soc-filter:focus {
outline: none;
border-color: var(--color-accent-border);
}
.soc-nav-group {
margin-bottom: 1.25rem;
}
.soc-nav-group-title {
color: var(--color-text-subtle);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.4rem;
}
.soc-sidebar ul {
list-style: none;
margin: 0;
padding: 0;
}
.soc-nav-link {
display: block;
color: var(--color-text);
text-decoration: none;
font-size: 0.85rem;
padding: 0.3rem 0 0.3rem 0.75rem;
border-left: 1px solid var(--color-border);
transition: color 0.2s, border-color 0.2s;
}
.soc-nav-link:hover {
color: var(--color-heading-secondary);
border-left-color: var(--color-border-hover);
}
.soc-nav-link.active {
color: var(--color-accent);
border-left-color: var(--color-accent);
}
.soc-nav-link.hidden {
display: none;
}
/* ── On this page ── */
.soc-toc {
position: sticky;
top: 0;
align-self: start;
max-height: 100vh;
overflow-y: auto;
padding: 4rem 0 2rem;
font-size: 0.8rem;
}
.soc-toc-title {
color: var(--color-text-subtle);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.6rem;
}
.soc-toc ul {
list-style: none;
margin: 0;
padding: 0;
}
.soc-toc a {
display: block;
color: var(--color-text-subtle);
text-decoration: none;
padding: 0.2rem 0;
transition: color 0.2s;
}
.soc-toc .soc-toc-level-3 a {
padding-left: 0.85rem;
}
.soc-toc a:hover {
color: var(--color-heading-secondary);
}
.soc-toc a.active {
color: var(--color-accent);
}
/* ── Landing hero ── */
.soc-hero {
margin-bottom: 3rem;
}
.soc-hero-title {
color: var(--color-heading);
font-size: clamp(2rem, 4vw, 2.75rem);
letter-spacing: -0.02em;
line-height: 1.15;
margin: 0 0 1rem;
}
.soc-hero-summary {
color: var(--color-heading-secondary);
font-size: 1.15rem;
line-height: 1.7;
margin: 0 0 1.5rem;
}
.soc-hero-cta {
display: inline-block;
color: var(--color-bg);
background: var(--color-accent);
font-weight: 600;
font-size: 0.9rem;
text-decoration: none;
padding: 0.55rem 1.1rem;
border-radius: 6px;
transition: background 0.2s;
}
.soc-hero-cta:hover {
background: var(--color-accent-hover);
}
/* ── Chapter cards ── */
.soc-chapters {
margin-top: 4rem;
}
.soc-chapters-title {
color: var(--color-heading);
font-size: 1.4rem;
margin: 0 0 1.25rem;
}
.soc-chapters-group {
color: var(--color-text-subtle);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
margin: 2rem 0 0.75rem;
}
.soc-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.soc-card {
display: flex;
flex-direction: column;
gap: 0.4rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 1.1rem 1.2rem;
text-decoration: none;
transition: background 0.2s, border-color 0.2s, transform 0.2s;
}
.soc-card:hover {
background: var(--color-surface-hover);
border-color: var(--color-accent-border);
transform: translateY(-2px);
}
.soc-card-title {
color: var(--color-heading-secondary);
font-weight: 600;
}
.soc-card-summary {
color: var(--color-text);
font-size: 0.85rem;
line-height: 1.5;
flex: 1;
}
.soc-card-meta {
color: var(--color-text-subtle);
font-size: 0.75rem;
}
/* ── Document header ── */
.soc-breadcrumbs {
display: flex;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--color-text-subtle);
}
.soc-breadcrumbs a {
color: var(--color-text-subtle);
text-decoration: none;
transition: color 0.2s;
}
.soc-breadcrumbs a:hover {
color: var(--color-heading-secondary);
}
.soc-doc-header {
margin: 1.5rem 0 2.5rem;
}
.soc-doc-title {
color: var(--color-heading);
font-size: clamp(1.75rem, 3vw, 2.25rem);
letter-spacing: -0.02em;
line-height: 1.25;
margin: 0 0 0.75rem;
}
.soc-doc-summary {
color: var(--color-heading-secondary);
font-size: 1.05rem;
margin: 0 0 0.5rem;
}
.soc-doc-meta {
color: var(--color-text-subtle);
font-size: 0.8rem;
}
/* ── Rendered markdown ── */
.soc-content {
line-height: 1.8;
}
.soc-content > h1:first-child {
display: none; /* the page header already shows the title */
}
.soc-content h2,
.soc-content h3,
.soc-content h4 {
position: relative;
color: var(--color-heading-secondary);
letter-spacing: -0.01em;
margin: 2.5rem 0 0.75rem;
}
.soc-content h2 {
font-size: 1.45rem;
padding-top: 1.5rem;
border-top: 1px solid var(--color-border);
}
.soc-content h3 { font-size: 1.15rem; }
.soc-anchor {
position: absolute;
left: -1.2em;
color: var(--color-text-subtle);
text-decoration: none;
opacity: 0;
transition: opacity 0.2s;
}
.soc-content h2:hover .soc-anchor,
.soc-content h3:hover .soc-anchor {
opacity: 1;
}
.soc-content p {
margin: 0 0 1.25rem;
}
.soc-content a {
color: var(--color-accent);
text-decoration: none;
}
.soc-content a:hover {
color: var(--color-accent-hover);
text-decoration: underline;
}
.soc-content strong {
color: var(--color-heading-secondary);
}
.soc-content ul,
.soc-content ol {
margin: 0 0 1.25rem 1.5rem;
padding: 0;
}
.soc-content li {
margin-bottom: 0.4rem;
}
.soc-content blockquote {
margin: 1.5rem 0;
padding: 0.75rem 1.25rem;
border-left: 3px solid var(--color-accent);
background: var(--color-surface);
color: var(--color-heading-secondary);
border-radius: 0 8px 8px 0;
}
.soc-content blockquote p:last-child {
margin-bottom: 0;
}
.soc-content hr {
border: none;
border-top: 1px solid var(--color-border);
margin: 2.5rem 0;
}
.soc-content code {
background: var(--color-surface-raised);
color: var(--color-heading-secondary);
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-size: 0.85em;
font-family: var(--font-mono);
}
.soc-content table {
width: 100%;
border-collapse: collapse;
margin: 0 0 1.5rem;
font-size: 0.9rem;
display: block;
overflow-x: auto;
}
.soc-content th,
.soc-content td {
border: 1px solid var(--color-border);
padding: 0.5rem 0.75rem;
text-align: left;
vertical-align: top;
}
.soc-content th {
background: var(--color-surface-raised);
color: var(--color-heading-secondary);
}
.soc-content details {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 0.75rem 1rem;
margin: 0 0 1.25rem;
}
.soc-content summary {
cursor: pointer;
color: var(--color-heading-secondary);
font-weight: 600;
}
.soc-content .footnotes {
font-size: 0.85rem;
color: var(--color-text-subtle);
}
/* ── Pager ── */
.soc-pager {
display: flex;
gap: 1rem;
margin-top: 4rem;
}
.soc-pager-link {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 0.9rem 1.1rem;
text-decoration: none;
transition: border-color 0.2s, background 0.2s;
}
.soc-pager-link.next {
text-align: right;
margin-left: auto;
}
.soc-pager-link:hover {
border-color: var(--color-accent-border);
background: var(--color-surface);
}
.soc-pager-label {
color: var(--color-text-subtle);
font-size: 0.75rem;
}
.soc-pager-title {
color: var(--color-heading-secondary);
font-weight: 600;
}
/* ── Mobile ── */
.soc-menu-toggle {
display: none;
}
@media (max-width: 1200px) {
.soc-shell {
grid-template-columns: 230px minmax(0, 760px);
}
.soc-toc {
display: none;
}
}
@media (max-width: 860px) {
.soc-shell {
grid-template-columns: minmax(0, 1fr);
}
.soc-main {
padding-top: 4.5rem;
}
.soc-menu-toggle {
display: block;
position: fixed;
top: 1rem;
left: 1rem;
z-index: 15;
background: var(--color-surface-raised);
backdrop-filter: blur(8px);
border: 1px solid var(--color-border);
border-radius: 6px;
color: var(--color-heading-secondary);
font: inherit;
font-size: 0.85rem;
padding: 0.4rem 0.8rem;
cursor: pointer;
}
.soc-sidebar {
position: fixed;
inset: 0 30% 0 0;
min-width: 260px;
max-height: none;
background: var(--color-bg);
border-right: 1px solid var(--color-border);
padding: 4.5rem 1.25rem 2rem;
z-index: 10;
transform: translateX(-100%);
transition: transform 0.25s ease;
}
.soc-sidebar.open {
transform: translateX(0);
}
.soc-pager {
flex-direction: column;
}
}
@@ -0,0 +1,86 @@
document.addEventListener('DOMContentLoaded', () => {
addHeadingAnchors();
trackReadingProgress();
highlightCurrentSection();
setUpSidebarFilter();
setUpMobileMenu();
});
// Hover "#" links on headings so any section can be linked to directly.
function addHeadingAnchors() {
document.querySelectorAll('.soc-content h2[id], .soc-content h3[id]').forEach(heading => {
const anchor = document.createElement('a');
anchor.className = 'soc-anchor';
anchor.href = '#' + heading.id;
anchor.textContent = '#';
anchor.setAttribute('aria-label', 'Link to this section');
heading.prepend(anchor);
});
}
function trackReadingProgress() {
const bar = document.getElementById('socProgress');
if (!bar) return;
const update = () => {
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
bar.style.width = scrollable > 0 ? (window.scrollY / scrollable) * 100 + '%' : '0';
};
window.addEventListener('scroll', update, { passive: true });
update();
}
// Highlights the "On this page" entry for the section currently being read.
function highlightCurrentSection() {
const links = [...document.querySelectorAll('.soc-toc a[data-toc-id]')];
if (links.length === 0) return;
const headings = links
.map(link => document.getElementById(link.dataset.tocId))
.filter(Boolean);
const update = () => {
let current = headings[0];
for (const heading of headings) {
if (heading.getBoundingClientRect().top <= 120) current = heading;
}
links.forEach(link => link.classList.toggle('active', link.dataset.tocId === current?.id));
};
window.addEventListener('scroll', update, { passive: true });
update();
}
function setUpSidebarFilter() {
const input = document.getElementById('socFilter');
if (!input) return;
input.addEventListener('input', () => {
const query = input.value.trim().toLowerCase();
document.querySelectorAll('.soc-nav-link').forEach(link => {
link.classList.toggle('hidden', query !== '' && !link.dataset.filterText.includes(query));
});
document.querySelectorAll('.soc-nav-group').forEach(group => {
const anyVisible = group.querySelector('.soc-nav-link:not(.hidden)');
group.style.display = anyVisible ? '' : 'none';
});
});
}
function setUpMobileMenu() {
const toggle = document.getElementById('socMenuToggle');
const sidebar = document.getElementById('socSidebar');
if (!toggle || !sidebar) return;
const setOpen = open => {
sidebar.classList.toggle('open', open);
toggle.setAttribute('aria-expanded', String(open));
toggle.textContent = open ? 'Close' : 'Contents';
};
toggle.addEventListener('click', () => setOpen(!sidebar.classList.contains('open')));
sidebar.addEventListener('click', event => {
if (event.target.closest('a')) setOpen(false);
});
}