diff --git a/artifacts/four-cuts-1.html b/artifacts/four-cuts-1.html index aecb513..40a9964 100644 --- a/artifacts/four-cuts-1.html +++ b/artifacts/four-cuts-1.html @@ -15,12 +15,13 @@ + @@ -117,7 +122,7 @@

Wealth and tax, each cut its own honest way.

You asked to see wealth and tax burden per bracket, total and per person, as stacks of $. Two of these are cleanly measurable by tax bracket; two are not. Rather than fake the alignment, each measure sits on the finest axis its real data supports — and the seam between them is marked, not hidden. Tap on any field to focus it full-screen.

-
+
2026 federal brackets — single filer · matched income group
10%$0 – $12,400inc <20%
12%$12,401 – $50,40020–40%
@@ -131,7 +136,7 @@

Wealth and tax, each cut its own honest way.

-
+
1 · Total tax generated per bracket
each $ = $1 billion of federal income tax
@@ -149,7 +154,7 @@

Wealth and tax, each cut its own honest way.

-
+
2 · Tax per person, per bracket
each $ = $1,000 of tax generated at that rate, per return reaching it
@@ -170,7 +175,7 @@

Wealth and tax, each cut its own honest way.

-
+
3 · Total net dollars held per income group
each $ = $1 billion of household net dollars (assets − debts)
@@ -188,7 +193,7 @@

Wealth and tax, each cut its own honest way.

-
+
4 · Net dollars held per person
each $ = $1,000 a person holds (assets − debts) — not what they're worth
@@ -206,7 +211,7 @@

Wealth and tax, each cut its own honest way.

-
+
hmmm

The two axes are not the same cut. Tax data (1, 2) is real, ranked by marginal rate — IRS reports exactly how much each rate generates and who reaches it. Wealth data (3, 4) is real, ranked by income group — the SCF reports net worth by income sextile. The bracket table up top pairs them by income range, but a taxpayer's taxable income (after deductions) and a family's usual income aren't the same number, so the pairing is a guide, not an identity.

Why wealth isn't drawn per bracket at all: the wealth-by-tax-bracket cross-tab isn't published, and any estimate would collapse at the top — the SCF's top income decile holds mean net dollars of $6.63M, which hides the top 1% holding near $44M. Forcing wealth onto the 7 brackets would understate the tail so badly it would lie. The income-sextile cut is the finest honest one, and even it buries that top-1% tail inside the 90–100 slice.

@@ -216,7 +221,7 @@

Wealth and tax, each cut its own honest way.

-
+

The interactive fields above (especially when expanded ⤢) make strong screenshots for threads. Below are the clean numbers + a suggested starter post you can copy straight into X. Feel free to quote any pie or field directly.

@@ -504,4 +509,4 @@

Wealth and tax, each cut its own honest way.

})(); - \ No newline at end of file + diff --git a/fallback/fallback.css b/fallback/fallback.css index 5c074fa..c985fe7 100644 --- a/fallback/fallback.css +++ b/fallback/fallback.css @@ -1 +1 @@ -:root{color-scheme:dark;--bg:#090d18;--panel:#131c30;--text:#eef3ff;--muted:#aebbd2;--line:#334361;--accent:#9b87f5;--amber:#f0c36a}*{box-sizing:border-box}body{max-width:52rem;margin:auto;padding:2rem 1rem;background:var(--bg);color:var(--text);font:18px/1.65 system-ui,sans-serif}header,section,footer{padding:1.25rem;margin:1rem 0;border:1px solid var(--line);border-radius:1rem;background:var(--panel)}h1,h2{font-family:Georgia,serif}h1{font-size:clamp(2.5rem,8vw,5rem);line-height:1;margin:.25rem 0}a{color:#c4bbff}.flag,.hmmm h2{color:var(--amber);font-weight:800;text-transform:uppercase;letter-spacing:.08em;font-size:.8rem}.hmmm{border-left:.35rem solid var(--amber)} +:root{color-scheme:dark;--bg:#090d18;--panel:#131c30;--text:#eef3ff;--muted:#aebbd2;--line:#334361;--accent:#9b87f5;--amber:#f0c36a}*{box-sizing:border-box}body{max-width:52rem;margin:auto;padding:2rem 1rem;background:var(--bg);color:var(--text);font:18px/1.65 system-ui,sans-serif}header,section,footer{padding:1.25rem;margin:1rem 0;border:1px solid var(--line);border-radius:1rem;background:var(--panel)}h1,h2{font-family:Georgia,serif}h1{font-size:clamp(2.5rem,8vw,5rem);line-height:1;margin:.25rem 0}a{color:#c4bbff}.flag,.hmmm h2{color:var(--amber);font-weight:800;text-transform:uppercase;letter-spacing:.08em;font-size:.8rem}.hmmm{border-left:.35rem solid var(--amber)}.copy-field{position:relative}.copy-button{float:right;min-height:2.2rem;margin:0 0 .75rem .75rem;padding:.4rem .65rem;border:1px solid var(--line);border-radius:.55rem;color:var(--text);background:var(--bg);font:700 .73rem/1 system-ui,sans-serif;cursor:pointer}.copy-button:hover{border-color:var(--accent)}.copy-status{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap} diff --git a/fallback/index.html b/fallback/index.html index 222c8e5..185d041 100644 --- a/fallback/index.html +++ b/fallback/index.html @@ -1 +1 @@ -The Interdependent Way — emergency static edition

Emergency static edition

The Interdependent Way

This dependency-free page exists for explicit recovery deployment. The last successful full site should remain live when ordinary builds fail.

Begin

The Interdependent Way is a dense living text and a constellation of projects attempting to explain, test, implement, challenge, and preserve it.

Primary links

hmmm

This edition contains no dynamic project snapshot, search index, Article Lab, or external dependencies. It is a recovery floor, not the preferred public experience.

Apache-2.0 unless an individual artifact states otherwise. Contact: curious@interdependentway.org

+The Interdependent Way — emergency static edition

Emergency static edition

The Interdependent Way

This static page exists for explicit recovery deployment. It remains readable without JavaScript; copy actions enhance it where supported. The last successful full site should remain live when ordinary builds fail.

Begin

The Interdependent Way is a dense living text and a constellation of projects attempting to explain, test, implement, challenge, and preserve it.

Primary links

hmmm

This edition contains no dynamic project snapshot, search index, Article Lab, or external dependencies. It is a recovery floor, not the preferred public experience.

Apache-2.0 unless an individual artifact states otherwise. Contact: curious@interdependentway.org

diff --git a/src/_includes/layouts/splash.njk b/src/_includes/layouts/splash.njk index 8ec33d0..d84273b 100644 --- a/src/_includes/layouts/splash.njk +++ b/src/_includes/layouts/splash.njk @@ -29,6 +29,7 @@ + diff --git a/src/assets/css/site.css b/src/assets/css/site.css index 89cdd30..9009876 100644 --- a/src/assets/css/site.css +++ b/src/assets/css/site.css @@ -54,6 +54,16 @@ h3 { font-size: clamp(1.2rem, 2vw, 1.55rem); } .actions { display: flex; gap: .75rem; flex-wrap: wrap; margin-top: 1.7rem; } .button { display: inline-flex; align-items: center; justify-content: center; min-height: 2.8rem; padding: .7rem 1rem; border: 1px solid var(--violet); border-radius: .75rem; color: var(--starlight); background: rgba(155, 135, 245, .1); text-decoration: none; font-weight: 700; } .button.secondary { border-color: var(--line); background: transparent; color: var(--silver); } +.copy-field { position: relative; } +.copy-button { float: right; min-height: 2.2rem; margin: 0 0 .75rem .75rem; padding: .4rem .65rem; border: 1px solid var(--line); border-radius: .55rem; color: var(--silver); background: var(--night); font: 700 .73rem/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; cursor: pointer; } +.copy-button:hover { color: var(--starlight); border-color: var(--violet); background: var(--night-soft); } +.copy-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; } +.copy-field-link { position: relative; min-width: 0; } +.copy-field-link > .card { height: 100%; } +.copy-field-link > .copy-button { position: absolute; z-index: 2; top: .75rem; right: .75rem; float: none; margin: 0; } +details.copy-field { padding-top: 1.2rem; } +details.copy-field > .copy-button { position: absolute; top: .65rem; right: .65rem; float: none; margin: 0; } +details.copy-field > summary { padding-right: 4.75rem; } .section-intro { max-width: 65ch; color: var(--silver); } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); gap: 1rem; margin: 1.4rem 0 3rem; } .card { display: block; position: relative; padding: 1.25rem; color: inherit; text-decoration: none; background: linear-gradient(145deg, rgba(22, 33, 58, .9), rgba(16, 24, 42, .86)); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: 0 0 0 rgba(0,0,0,0); transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; } @@ -174,4 +184,4 @@ summary { cursor: pointer; font-weight: 800; } .hmmm-boundary { grid-column: auto; grid-template-columns: 1fr; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } } -@media print { body { background: white; color: black; } body::before, .site-header, .actions, .noscript { display: none; } .site-main { max-width: none; padding: 0; } .card, .panel, .provenance, .hmmm, details, .textbook-chapter { background: white; color: black; box-shadow: none; } a { color: black; } } +@media print { body { background: white; color: black; } body::before, .site-header, .actions, .noscript, .copy-button { display: none; } .site-main { max-width: none; padding: 0; } .card, .panel, .provenance, .hmmm, details, .textbook-chapter { background: white; color: black; box-shadow: none; } a { color: black; } } diff --git a/src/assets/js/site.js b/src/assets/js/site.js index 7f79174..2c03eea 100644 --- a/src/assets/js/site.js +++ b/src/assets/js/site.js @@ -1,6 +1,6 @@ // === MODULE_BUILD === // id: optional_site_enhancement -// purpose: Add a compact mobile navigation toggle without hiding static content. +// purpose: Add compact mobile navigation and one-copy-action-per-text-field without hiding static content. // entrypoint: loaded with defer from the base layout // tests: tests/site-contract.test.mjs // === END MODULE_BUILD === @@ -17,3 +17,104 @@ if (button && nav) { nav.dataset.open = String(!open); }); } + +// A public field is a deliberate, self-contained reading unit: not a heading, +// navigation link, or decorative label. Keep this list aligned with the site +// component vocabulary so new content using an established field class gains a +// copy action automatically. `data-copy-field` is the escape hatch for a new +// field type that has not yet earned a shared class. +const COPYABLE_FIELD_SELECTOR = [ + '[data-copy-field]', + '#content .hero', + '#content .page-head', + '#content .panel', + '#content .provenance', + '#content .hmmm', + '#content .reading', + '#content .textbook-chapter', + '#content .source-block', + '#content .criterion', + '#content .journal-entry', + '#content details', + '#content .card', + '#content .awakening-copy', + '#content .awakening-text', + '#content .awakening-provenance', + '.site-footer .footer-provenance', + '.site-footer .hmmm-boundary' +].join(', '); + +function textForCopy(field) { + const copy = field.cloneNode(true); + copy.querySelectorAll('.copy-button, .copy-status, script, style, template, [data-copy-ignore]').forEach(node => node.remove()); + return copy.innerText.replace(/\n{3,}/g, '\n\n').trim(); +} + +function fieldLabel(field) { + const heading = field.querySelector('h1, h2, h3, summary, strong'); + const label = heading?.textContent?.replace(/\s+/g, ' ').trim(); + return label ? `Copy ${label}` : 'Copy field text'; +} + +async function writeText(text) { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return; + } + + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.setAttribute('readonly', ''); + textarea.style.cssText = 'position:fixed;opacity:0;pointer-events:none'; + document.body.append(textarea); + textarea.select(); + const copied = document.execCommand('copy'); + textarea.remove(); + if (!copied) throw new Error('Clipboard copy is unavailable.'); +} + +function addCopyControl(field) { + if (field.dataset.copyReady === 'true' || !textForCopy(field)) return; + + const control = document.createElement('button'); + control.type = 'button'; + control.className = 'copy-button'; + control.textContent = 'Copy'; + control.setAttribute('aria-label', fieldLabel(field)); + + const status = document.createElement('span'); + status.className = 'copy-status'; + status.setAttribute('role', 'status'); + status.setAttribute('aria-live', 'polite'); + + let controlField = field; + if (field.matches('a')) { + // An anchor cannot contain an interactive descendant. Its copy action is + // therefore a sibling in a small wrapper, while the copied text remains + // exactly the card/link field. + const wrapper = document.createElement('div'); + wrapper.className = 'copy-field copy-field-link'; + field.replaceWith(wrapper); + wrapper.append(control, status, field); + controlField = wrapper; + } else if (field.matches('details')) { + field.append(control, status); + } else { + field.prepend(control, status); + } + + controlField.classList.add('copy-field'); + field.dataset.copyReady = 'true'; + control.addEventListener('click', async () => { + try { + await writeText(textForCopy(field)); + control.textContent = 'Copied'; + status.textContent = 'Copied to clipboard.'; + window.setTimeout(() => { control.textContent = 'Copy'; }, 1600); + } catch { + status.textContent = 'Copy failed. Select the text and copy it manually.'; + } + }); +} + +document.querySelectorAll(COPYABLE_FIELD_SELECTOR).forEach(addCopyControl); diff --git a/tests/accessibility.spec.mjs b/tests/accessibility.spec.mjs index 47f7065..87ca2f4 100644 --- a/tests/accessibility.spec.mjs +++ b/tests/accessibility.spec.mjs @@ -14,7 +14,7 @@ if (!firstLabUnit) throw new Error(`missing Lab canon unit ${articleLab[0].unit_ for (const route of [ '/', '/home/', '/preamble/', '/chapters/', '/chapters/chapter-zero/', '/chapters/chapter-seven/', '/articles/', '/articles/article-two/', '/narratives/', '/narratives/jack-and-diane/', '/way/', '/lab/', `/lab/${firstLabUnit.routeSlug}/`, '/projects/', - '/artifacts/edcm-mathematics/', '/research/method/' + '/artifacts/edcm-mathematics/', '/artifacts/four-cuts/', '/fallback/', '/research/method/' ]) { test(`${route} has no serious or critical automated accessibility violations`, async ({ page }) => { await page.goto(route); diff --git a/tests/site.spec.mjs b/tests/site.spec.mjs index fd95697..2606ac9 100644 --- a/tests/site.spec.mjs +++ b/tests/site.spec.mjs @@ -46,6 +46,7 @@ test('Awakening is the public splash and preserves one-click continuation', asyn await expect(page.locator('.awakening-splash')).toBeVisible(); await expect(page.locator('h1')).toHaveText('Awakening'); await expect(page.locator('.awakening-text')).toContainText('You are not alone'); + await expect(page.locator('.awakening-text > .copy-button')).toHaveCount(1); await expect(page.locator('.site-header')).toHaveCount(0); const preambleLink = page.locator('a[href="/preamble/"]', { hasText: 'Read the Preamble' }).first(); @@ -59,6 +60,46 @@ test('Awakening is the public splash and preserves one-click continuation', asyn await expect(page.locator('.source-block')).toContainText('Humanity faces extinction'); }); +test('every established text-field type receives one working copy control', async ({ page, context }) => { + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + + await page.goto('/articles/article-two/'); + for (const selector of ['.page-head', '.panel', '.reading', '.card', '.hmmm']) { + const field = page.locator(selector).first(); + await expect(field.locator(':scope > .copy-button')).toHaveCount(1); + } + + const reading = page.locator('.reading').first(); + const copy = reading.locator(':scope > .copy-button'); + await copy.click(); + await expect(copy).toHaveText('Copied'); + await expect(reading.locator(':scope > .copy-status')).toHaveText('Copied to clipboard.'); + await expect.poll(async () => page.evaluate(() => navigator.clipboard.readText())).toContain('None shall be enslaved'); + + await page.goto('/chapters/chapter-zero/'); + await expect(page.locator('.provenance > .copy-button')).toHaveCount(1); + await expect(page.locator('.textbook-chapter > .copy-button')).toHaveCount(1); + + await page.goto('/way/'); + const unit = page.locator('details.canon-unit').first(); + await expect(unit.locator(':scope > .copy-button')).toHaveCount(1); + await expect(unit.locator('.source-block > .copy-button')).toHaveCount(1); + + await page.goto('/chapters/'); + const linkedCard = page.locator('.copy-field-link').first(); + await expect(linkedCard.locator(':scope > .copy-button')).toHaveCount(1); + await expect(linkedCard.locator('a .copy-button')).toHaveCount(0); + + await page.goto('/fallback/'); + await expect(page.locator('header > .copy-button')).toHaveCount(1); + await expect(page.locator('main section').first().locator(':scope > .copy-button')).toHaveCount(1); + + await page.goto('/artifacts/four-cuts/'); + await expect(page.locator('.bracket-ref > .copy-button')).toHaveCount(1); + await expect(page.locator('.measure').first().locator(':scope > .copy-button')).toHaveCount(1); + await expect(page.locator('.x-ready > .copy-button')).toHaveCount(1); +}); + test('the knowledge-system home links to Awakening, Preamble, and the distributed textbook', async ({ page }) => { await page.goto('/home/'); await expect(page.locator('a.brand')).toHaveAttribute('href', '/home/');