diff --git a/scripts/debug-sticky.mjs b/scripts/debug-sticky.mjs
index 9da2982..b0c4f62 100644
--- a/scripts/debug-sticky.mjs
+++ b/scripts/debug-sticky.mjs
@@ -2,7 +2,7 @@ import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
-await page.goto('http://localhost:5176/stack-folder/', { waitUntil: 'networkidle' });
+await page.goto('http://localhost:5173/stack-folder/', { waitUntil: 'networkidle' });
for (const y of [0, 400, 800, 1200]) {
await page.evaluate((sy) => window.scrollTo(0, sy), y);
diff --git a/scripts/test-stack-cover.mjs b/scripts/test-stack-cover.mjs
new file mode 100644
index 0000000..978e3d0
--- /dev/null
+++ b/scripts/test-stack-cover.mjs
@@ -0,0 +1,50 @@
+import { chromium } from 'playwright';
+
+const BASE = process.env.BASE_URL || 'http://localhost:5173';
+const REVEAL = 48 + 44 * 6 + 32; // approx px
+
+async function testCover(page, path, bodySel, getTitle) {
+ await page.goto(BASE + path, { waitUntil: 'networkidle' });
+ const results = [];
+ for (const y of [0, 600, 1200, 1800]) {
+ await page.evaluate((sy) => window.scrollTo(0, sy), y);
+ await page.waitForTimeout(250);
+ const r = await page.evaluate(({ bodySel, reveal }) => {
+ const bodies = [...document.querySelectorAll(bodySel)];
+ const stacked = bodies.filter((b) => {
+ const br = b.getBoundingClientRect();
+ return Math.abs(br.top - reveal) < 30 && br.height > 100;
+ });
+ const top = stacked.sort((a, b) => {
+ const az = parseInt(getComputedStyle(a.closest('.folder, .layer, .frame, .unit') || a).zIndex) || 0;
+ const bz = parseInt(getComputedStyle(b.closest('.folder, .layer, .frame, .unit') || b).zIndex) || 0;
+ return bz - az;
+ })[0];
+ const el = document.elementFromPoint(innerWidth / 2, reveal + 120);
+ const hit = el?.closest('.folder, .layer, .frame, .unit');
+ const layer = hit?.closest('[data-layer]')?.dataset?.layer ?? hit?.className?.match(/f(\d)|layer-(\d)|u(\d)/)?.[1];
+ return {
+ scrollY: scrollY,
+ stackedCount: stacked.length,
+ topZ: top ? getComputedStyle(top.closest('.folder, .layer, .frame, .unit')).zIndex : null,
+ hitLayer: layer,
+ hitText: hit?.querySelector('h1, h2, strong')?.textContent?.slice(0, 40),
+ };
+ }, { bodySel, reveal: REVEAL });
+ results.push(r);
+ }
+ return results;
+}
+
+const browser = await chromium.launch({ headless: true });
+const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
+
+const folder = await testCover(page, '/stack-folder/', '.folder .body', (b) => b.querySelector('h1,h2')?.textContent);
+console.log('folder', JSON.stringify(folder, null, 2));
+
+await page.screenshot({ path: '/tmp/levkin-cover-folder-1200.png' });
+await page.evaluate(() => window.scrollTo(0, 1200));
+await page.waitForTimeout(300);
+await page.screenshot({ path: '/tmp/levkin-cover-folder-1200b.png' });
+
+await browser.close();
diff --git a/scripts/test-stack.mjs b/scripts/test-stack.mjs
index 8b1d80a..237d4f6 100644
--- a/scripts/test-stack.mjs
+++ b/scripts/test-stack.mjs
@@ -2,90 +2,75 @@ import { chromium } from 'playwright';
import { writeFileSync, mkdirSync } from 'fs';
import { join } from 'path';
-const BASE = process.env.BASE_URL || 'http://localhost:5176';
+const BASE = process.env.BASE_URL || 'http://localhost:5173';
const OUT = '/tmp/levkin-stack-test';
mkdirSync(OUT, { recursive: true });
const pages = [
- { name: 'stack', path: '/stack/' },
- { name: 'stack-folder', path: '/stack-folder/' },
- { name: 'stack-trace', path: '/stack-trace/' },
- { name: 'stack-rack', path: '/stack-rack/' },
+ { name: 'stack', path: '/stack/', bodySel: '.layer-inner' },
+ { name: 'stack-folder', path: '/stack-folder/', bodySel: '.folder .body' },
+ { name: 'stack-trace', path: '/stack-trace/', bodySel: '.frame-body' },
+ { name: 'stack-rack', path: '/stack-rack/', bodySel: '.unit-body' },
];
const scrollY = [0, 400, 800, 1200, 1800, 2400];
-async function analyze(page, label) {
- return page.evaluate(() => {
- const sections = [...document.querySelectorAll('.scroll-section')];
- const bodies = [...document.querySelectorAll('.body, .layer-inner, .frame-body, .unit-body')];
- const tabs = [...document.querySelectorAll('.tab, .frame-line, .unit-head')];
- const sticky = [...document.querySelectorAll('.tab, .body, .layer-inner, .frame-line, .frame-body, .unit-head, .unit-body')];
- const rects = (els) => els.map((el, i) => {
- const r = el.getBoundingClientRect();
- const cs = getComputedStyle(el);
- return {
- i,
- tag: el.className?.slice?.(0, 40) || el.tagName,
- top: Math.round(r.top),
- bottom: Math.round(r.bottom),
- height: Math.round(r.height),
- position: cs.position,
- zIndex: cs.zIndex,
- visible: r.height > 0 && r.bottom > 0 && r.top < innerHeight,
- };
+async function analyze(page, bodySel) {
+ const reveal = await page.evaluate(() => {
+ const s = getComputedStyle(document.documentElement);
+ return parseFloat(s.getPropertyValue('--stack-reveal')) ||
+ (48 + 44 * 6 + 32);
+ });
+
+ return page.evaluate(({ bodySel, reveal }) => {
+ const bodies = [...document.querySelectorAll(bodySel)];
+ const stacked = bodies.filter((b) => {
+ const r = b.getBoundingClientRect();
+ return Math.abs(r.top - reveal) < 35 && r.height > 80;
});
- const visibleBodies = rects(bodies).filter((b) => b.visible && b.height > 80);
- const mount = document.querySelector('.mount, .stack-mount');
- const mountH = mount ? Math.round(mount.getBoundingClientRect().height) : 0;
+ const visible = bodies.filter((b) => {
+ const r = b.getBoundingClientRect();
+ return r.height > 80 && r.bottom > 0 && r.top < innerHeight;
+ });
+ const el = document.elementFromPoint(innerWidth / 2, reveal + 100);
+ const panel = el?.closest('.folder, .layer, .frame, .unit');
+ const layer = panel?.closest('[data-layer]')?.dataset?.layer;
+ const title = panel?.querySelector('h1, h2, strong')?.textContent?.trim().slice(0, 50);
return {
scrollY: Math.round(window.scrollY),
- pageH: Math.round(document.documentElement.scrollHeight),
- viewport: innerHeight,
- sections: sections.length,
- visibleBodyCount: visibleBodies.length,
- visibleBodies,
- mountDocHeight: mountH,
- firstSectionTop: sections[0] ? Math.round(sections[0].getBoundingClientRect().top) : null,
- lastSectionBottom: sections.at(-1) ? Math.round(sections.at(-1).getBoundingClientRect().bottom) : null,
- stickyPositions: rects(sticky.filter((el) => getComputedStyle(el).position === 'sticky')).slice(0, 14),
+ reveal: Math.round(reveal),
+ stackedCount: stacked.length,
+ visibleCount: visible.length,
+ topLayer: layer,
+ topTitle: title,
+ issues: [],
};
- });
+ }, { bodySel, reveal });
}
const browser = await chromium.launch({ headless: true });
const report = [];
-for (const { name, path } of pages) {
+for (const { name, path, bodySel } of pages) {
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
- const url = BASE + path;
- await page.goto(url, { waitUntil: 'networkidle' });
+ await page.goto(BASE + path, { waitUntil: 'networkidle' });
const shots = [];
for (const y of scrollY) {
await page.evaluate((sy) => window.scrollTo(0, sy), y);
- await page.waitForTimeout(200);
- const data = await analyze(page, name);
- const file = join(OUT, `${name}-scroll-${y}.png`);
- await page.screenshot({ path: file, fullPage: false });
- shots.push({ y, file, ...data });
+ await page.waitForTimeout(250);
+ const data = await analyze(page, bodySel);
+ if (data.stackedCount < 2 && y > 200) data.issues.push('not stacking (need 2+ bodies at reveal line)');
+ if (data.visibleCount > 2 && y > 200) data.issues.push(`${data.visibleCount} bodies spread out, not covering`);
+ await page.screenshot({ path: join(OUT, `${name}-scroll-${y}.png`) });
+ shots.push({ y, ...data });
}
- report.push({ name, url, shots });
+ report.push({ name, shots });
await page.close();
}
await browser.close();
-
-const summary = report.map((r) => {
- const problems = r.shots.map((s) => {
- const issues = [];
- if (s.visibleBodyCount > 2) issues.push(`${s.visibleBodyCount} bodies visible (want ≤2)`);
- if (s.scrollY === 0 && s.visibleBodyCount > 1) issues.push('top: multiple full bodies');
- if (s.pageH > s.viewport * 8) issues.push(`page very tall: ${s.pageH}px`);
- return { y: s.y, issues, visibleBodyCount: s.visibleBodyCount, pageH: s.pageH };
- });
- return { variant: r.name, url: r.url, scrollChecks: problems };
-});
-
-writeFileSync(join(OUT, 'report.json'), JSON.stringify({ report, summary }, null, 2));
-console.log(JSON.stringify(summary, null, 2));
-console.log('\nScreenshots:', OUT);
+writeFileSync(join(OUT, 'report.json'), JSON.stringify(report, null, 2));
+console.log(JSON.stringify(report.map((r) => ({
+ variant: r.name,
+ checks: r.shots.map((s) => ({ y: s.y, stacked: s.stackedCount, top: s.topTitle, issues: s.issues })),
+})), null, 2));
diff --git a/shared/stack-layout.css b/shared/stack-layout.css
index 49ef3cf..1a74614 100644
--- a/shared/stack-layout.css
+++ b/shared/stack-layout.css
@@ -1,4 +1,4 @@
-/* Pull layers into one stack so sticky overlap works */
+/* Overlapping scroll slots — each layer covers the one below via z-index */
.scroll-section {
height: var(--stack-slot);
position: relative;
@@ -19,17 +19,17 @@
margin-bottom: 3rem;
}
-/* Only the active layer shows its body; tabs always visible */
-.scroll-section:not(.is-active) .body,
-.scroll-section:not(.is-active) .layer-inner,
-.scroll-section:not(.is-active) .frame-body,
-.scroll-section:not(.is-active) .unit-body {
+/* Body visible only when card is stuck on the stack — next card covers previous */
+.scroll-section:not(.is-stuck) .body,
+.scroll-section:not(.is-stuck) .layer-inner,
+.scroll-section:not(.is-stuck) .frame-body,
+.scroll-section:not(.is-stuck) .unit-body {
visibility: hidden;
- height: 0;
min-height: 0;
- margin-top: 0 !important;
- padding: 0;
+ height: 0;
+ padding-top: 0;
+ padding-bottom: 0;
border: none;
- overflow: hidden;
box-shadow: none;
+ overflow: hidden;
}
diff --git a/shared/stack-scroll.js b/shared/stack-scroll.js
index 5426fed..9defb81 100644
--- a/shared/stack-scroll.js
+++ b/shared/stack-scroll.js
@@ -1,4 +1,4 @@
-/** Shared scroll-depth tracking + jump-to-layer for stack variants */
+/** Scroll depth + jump + hide upcoming cards until they cover the stack */
export function initStackScroll(options = {}) {
const {
sectionSelector = '.scroll-section',
@@ -11,14 +11,22 @@ export function initStackScroll(options = {}) {
const sections = document.querySelectorAll(sectionSelector);
if (!sections.length) return;
+ const reveal = () => {
+ const v = getComputedStyle(document.documentElement).getPropertyValue('--stack-reveal').trim();
+ return parseFloat(v) || 344;
+ };
+
const mid = () => window.innerHeight * 0.42;
function updateDepth() {
+ const rLine = reveal();
let active = 0;
+
sections.forEach((sec) => {
const r = sec.getBoundingClientRect();
if (r.top <= mid() && r.bottom > mid()) active = Number(sec.dataset.layer);
});
+
if (depthEl) depthEl.textContent = `${depthPrefix}${active}`;
document.querySelectorAll('.stack-ruler button, .stack-ruler [data-goto], .tab-rail button, .tab[data-goto]').forEach((el) => {
@@ -29,11 +37,17 @@ export function initStackScroll(options = {}) {
sections.forEach((sec) => {
const layer = Number(sec.dataset.layer);
- const isActive = layer === active;
- sec.classList.toggle('is-active', isActive);
const panel = sec.querySelector(panelSelector);
if (!panel) return;
- panel.style.zIndex = isActive ? 100 : 10 + layer;
+
+ const r = sec.getBoundingClientRect();
+ const pr = panel.getBoundingClientRect();
+ const onStack = Math.abs(pr.top - rLine) < 10;
+ const past = r.bottom <= rLine;
+
+ sec.classList.toggle('is-active', layer === active);
+ sec.classList.toggle('is-stuck', onStack);
+ sec.classList.toggle('is-past', past);
});
}
@@ -49,5 +63,6 @@ export function initStackScroll(options = {}) {
});
window.addEventListener('scroll', updateDepth, { passive: true });
+ window.addEventListener('resize', updateDepth, { passive: true });
updateDepth();
}
diff --git a/shared/stack-vars.css b/shared/stack-vars.css
index 6cebfc0..91d26ae 100644
--- a/shared/stack-vars.css
+++ b/shared/stack-vars.css
@@ -1,4 +1,4 @@
-/* Shared stack scroll — overlapping sticky layers */
+/* Shared stack scroll — cards share one sticky line and cover via z-index */
:root {
--stack-nav: 2.5rem;
--stack-stick: 3rem;
@@ -7,6 +7,7 @@
--stack-reveal: calc(var(--stack-stick) + var(--stack-step) * 6 + var(--stack-tab-h));
--stack-slot: 100vh;
--stack-slot-last: 50vh;
+ /* Scroll one "deck" height before next card covers */
--stack-pull: calc(var(--stack-slot) - var(--stack-reveal));
--stack-body-h: calc(100dvh - var(--stack-reveal) - 1.25rem);
}
diff --git a/spec/index.html b/spec/index.html
index ede0f08..fd12445 100644
--- a/spec/index.html
+++ b/spec/index.html
@@ -4,7 +4,7 @@
Levkin — Company Specification
-
+
@@ -25,15 +25,15 @@
-
-
+
+
+
+ Company Specification
+ Levkin Software Development Company
+ Master overview of services and engagement terms. This document is not a binding agreement; a statement of work is issued for each engagement.
-
- Abstract
- This document describes Levkin , a Canadian software development practice specializing in production systems, business automation, and enterprise tooling. Remote across North American and European time zones. Levkin ships software that must work when nobody is watching — with error handling, documentation, and tests as non-optional requirements.
- Quality engineering and SDET work are documented at iliadobkin.com (external) — an interactive portfolio (Playwright-style test runner UI, career timeline, trace-driven debugging showcase).
- AVAILABLE Currently taking on new engagements.
-
-
-
- 1. Scope
- Levkin operates as a boutique engineering practice, not a body shop. Engagements are scoped, shipped, and handed off with the expectation that the client (or their next hire) can maintain what was built.
- interface Engagement {
- discovery: "15min call, no obligation"; // cal.levkin.ca/ilia/consult
- delivery: "production-ready, documented";
- maintenance: "optional, not mandatory lock-in";
-}
-
-
-
- 2. Services
- The following endpoints represent primary service offerings. Each maps to a deployable capability.
-
-
-
- BUILD
- /custom-software
-
- Web applications, APIs, internal tools. Stack-agnostic; preference for boring, proven technology.
-
- TypeScript / Node · Python · C# / .NET
- PostgreSQL · SQL Server · SQLite
- React · Vue · server-rendered where appropriate
-
-
-
-
-
- Production-ready automation — scripts, no-code workflows, CI/CD, webhooks, AI integrations. Runs while you sleep.
-
- n8n · Zapier · Make · GitHub Actions · Jenkins · Azure DevOps
- Python · Node · Bash · macOS/iOS Shortcuts
- OpenAI · Claude · custom LLM pipelines
-
-
-
-
-
- CaseWare & CaseView features, client templates, release automation. 15+ years; teams at CaseWare International, MNP, JazzIt.
-
- C# · .NET · SQL Server · JavaScript automation
- Template delivery · CI/CD · mentorship · modernization
-
-
-
-
-
- Senior SDET services — test automation, CI/CD pipelines, trace-driven debugging, contract QA leadership.
-
- Portfolio: iliadobkin.com (external) — runnable career specs, trace viewer
- Remote (ET) · Canadian · git.levkin.ca for source
- Playwright / JS automation · enterprise CI (Jenkins, Azure DevOps, GitHub Actions)
-
-
-
-
-
- 3. Documented Outcomes
-
-
- Documented outcomes metrics
-
- Metric Value Context
-
+
-
- Engagement flow: Discover (15 min) → Design (proposal) → Ship (tested, documented) → Maintain (optional).
-
+
-
- 4. Required Properties
- All Levkin deliverables MUST satisfy the following constraints unless explicitly waived in writing.
-
-
- Required deliverable properties
+
+
— Preamble
+
Levkin Inc. builds and maintains software for businesses — custom applications, automation, and specialized practice systems. Work is scoped, documented, and built to run in production without hand-holding.
+
AVAILABLE Taking on new engagements. Remote across North American and European time zones.
+
+
+
+ 1. Scope
+ Boutique practice — not a staffing agency. Projects have a clear start, deliverable, and handoff so your team can own what ships.
+ Typical flow: a short discovery call, a fixed-scope proposal, delivery with documentation, then optional ongoing support if you want it.
+
+
+
+ 2. Services
+ Primary service lines. Scope, timeline, and fees for a given engagement are set out in a separate statement of work.
+
+
+
Custom software
+
Web apps, APIs, and internal tools — chosen to fit the problem, not a preset stack.
+
+
+
Automation
+
Workflows and integrations that run reliably in the background — reporting, notifications, data sync, and repetitive ops work off your plate. See auto.levkin.ca (external) .
+
+
+
CaseWare
+
Templates, releases, and practice customization for firms on CaseWare — from small fixes to full pipeline overhauls. See caseware.levkin.ca (external) .
+
+
+
Quality & testing
+
Test strategy, automation, and release confidence for teams that need an experienced QA lead without hiring full-time. See iliadobkin.com (external) .
+
+
+
+
+
+ 3. Terms of engagement
+
+
+
Fixed scope
+
Quoted after discovery; no open-ended hourly surprises.
+
+
+
Production-ready
+
Monitoring, failure handling, and documentation included by default.
+
+
+
Handoff-friendly
+
Your team or the next vendor can maintain what we ship.
+
+
+
Right-sized
+
The smallest solution that actually solves the problem.
+
+
+
+
+
+ 4. References
+
+ External references
- Property Requirement Rationale
+ Reference Purpose
- reliabilityRetries, alerts, graceful degradation Production ≠ demo
- documentationRunbook or README sufficient for handoff Bus factor > 1
- testabilityAutomated tests before live data Regressions are expensive
- pragmatismSmallest solution that solves the problem 20-line script > 200-node workflow
+ auto.levkin.ca (external) Automation
+ caseware.levkin.ca (external) CaseWare
+ cal.levkin.ca (external) Book a discovery call
-
-
+
-
- 5. Registered Subdomains
-
-
- Registered Levkin subdomains
-
- Host Purpose Status
-
+
+
+ To discuss a project, use either channel below. A formal statement of work follows acceptance of a written proposal.
+
-
-
-
-
-
+
+
+
+
diff --git a/spec/spec.css b/spec/spec.css
index 66dcaa6..334ffd3 100644
--- a/spec/spec.css
+++ b/spec/spec.css
@@ -1,84 +1,69 @@
-/* --- themes (semantic tokens — no blue-on-blue panels) --- */
+/* --- themes --- */
:root,
[data-theme="light"] {
color-scheme: light;
- --paper: #f6f3ee;
- --surface: #fffefb;
- --panel: #ebe8e2;
- --ink: #121211;
- --muted: #3d3b37;
- --rule: #c5bfb4;
+ --desk: #ddd9d2;
+ --paper: #f0ede8;
+ --sheet: #fffffe;
+ --panel: #f5f3ef;
+ --ink: #1a1a18;
+ --muted: #4a4844;
+ --rule: #c8c2b8;
+ --rule-strong: #1a1a18;
--link: #0b4a75;
--link-hover: #083558;
- --path: #0b4a75;
- --nav-active: #e4e0d8;
- --nav-active-border: #0b4a75;
- --code-bg: #e8e5df;
- --code-fg: #1a2e28;
- --table-head: #e4e0d8;
- --badge-ok-bg: #c8e6ce;
+ --nav-active: #ebe8e2;
+ --badge-ok-bg: #d8eadc;
--badge-ok-fg: #0f3d1a;
--badge-ok-border: #6b9e74;
- --badge-post-bg: #f0e0d0;
- --badge-post-fg: #5a2e0a;
- --badge-post-border: #c49a6a;
--focus: #0b4a75;
- --skip-bg: #121211;
- --skip-fg: #f6f3ee;
+ --skip-bg: #1a1a18;
+ --skip-fg: #fffffe;
+ --shadow: 0 2px 4px rgba(26, 26, 24, 0.06), 0 16px 48px rgba(26, 26, 24, 0.1);
}
[data-theme="dim"] {
color-scheme: light;
- --paper: #c9c3b8;
- --surface: #e3ded4;
- --panel: #d6d0c6;
- --ink: #12110f;
- --muted: #363430;
+ --desk: #b5afa4;
+ --paper: #ccc6bc;
+ --sheet: #ebe7e0;
+ --panel: #e0dbd3;
+ --ink: #1a1917;
+ --muted: #3d3b36;
--rule: #a39d92;
+ --rule-strong: #1a1917;
--link: #094264;
--link-hover: #062f48;
- --path: #094264;
- --nav-active: #bab4a8;
- --nav-active-border: #5c584f;
- --code-bg: #d8d2c8;
- --code-fg: #1a2822;
- --table-head: #d0cac0;
+ --nav-active: #d4cec4;
--badge-ok-bg: #a8c8ae;
--badge-ok-fg: #0a3318;
--badge-ok-border: #4a7a54;
- --badge-post-bg: #dcc8b4;
- --badge-post-fg: #4a2808;
- --badge-post-border: #a07850;
--focus: #094264;
- --skip-bg: #12110f;
- --skip-fg: #e3ded4;
+ --skip-bg: #1a1917;
+ --skip-fg: #ebe7e0;
+ --shadow: 0 2px 4px rgba(26, 25, 23, 0.08), 0 18px 52px rgba(26, 25, 23, 0.14);
}
[data-theme="dark"] {
color-scheme: dark;
- --paper: #10100f;
- --surface: #1a1a18;
+ --desk: #0a0a09;
+ --paper: #121211;
+ --sheet: #1c1c1a;
--panel: #242422;
- --ink: #eeece8;
- --muted: #c4c0b8;
- --rule: #3a3834;
- --link: #7ec8f4;
- --link-hover: #a8e0ff;
- --path: #9ed4f8;
+ --ink: #f0eeea;
+ --muted: #b8b4ac;
+ --rule: #3d3b38;
+ --rule-strong: #d8d4cc;
+ --link: #8ec8f0;
+ --link-hover: #b8e0ff;
--nav-active: #2a2a28;
- --nav-active-border: #7ec8f4;
- --code-bg: #1e1e1c;
- --code-fg: #c0d8cc;
- --table-head: #242422;
--badge-ok-bg: #1a3d28;
--badge-ok-fg: #b8e8c4;
--badge-ok-border: #4a8a5c;
- --badge-post-bg: #3d2a1c;
- --badge-post-fg: #f0d8c0;
- --badge-post-border: #8a6a48;
- --focus: #7ec8f4;
- --skip-bg: #eeece8;
- --skip-fg: #10100f;
+ --focus: #8ec8f0;
+ --skip-bg: #f0eeea;
+ --skip-fg: #121211;
+ --shadow: 0 2px 6px rgba(0, 0, 0, 0.25), 0 20px 56px rgba(0, 0, 0, 0.4);
}
@media (prefers-contrast: more) {
@@ -87,23 +72,23 @@
[data-theme="dim"] {
--muted: #1a1816;
--rule: #5a5650;
- --badge-ok-border: #0f3d1a;
- --badge-post-border: #5a2e0a;
}
-
[data-theme="dark"] {
- --muted: #eeece8;
+ --muted: #f0eeea;
--rule: #8a8680;
- --badge-ok-border: #b8e8c4;
- --badge-post-border: #f0d8c0;
}
}
:root {
--font-scale: 1;
--mono: 'IBM Plex Mono', ui-monospace, monospace;
- --serif: 'Literata', Georgia, serif;
- --focus-ring: 0 0 0 3px color-mix(in srgb, var(--focus) 35%, transparent);
+ --serif: 'Literata', Georgia, 'Times New Roman', serif;
+ --measure: 42rem;
+ --sidebar: 14.5rem;
+ --pad-x: 3rem;
+ --pad-y: 1.75rem;
+ --body-size: 0.9375rem;
+ --heading-track: 0.08em;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
@@ -119,16 +104,15 @@ html {
body {
display: grid;
- grid-template-columns: minmax(12rem, 220px) 1fr;
+ grid-template-columns: var(--sidebar) 1fr;
min-height: 100vh;
- background: var(--paper);
+ background: var(--desk);
color: var(--ink);
font-family: var(--serif);
- font-size: 1.05rem;
+ font-size: var(--body-size);
line-height: 1.65;
}
-/* screen reader only */
.sr-only {
position: absolute;
width: 1px;
@@ -142,7 +126,7 @@ body {
}
.skip-link {
- position: absolute;
+ position: fixed;
top: 0.5rem;
left: 0.5rem;
z-index: 100;
@@ -150,9 +134,8 @@ body {
background: var(--skip-bg);
color: var(--skip-fg);
font-family: var(--mono);
- font-size: 0.8rem;
+ font-size: 0.75rem;
text-decoration: none;
- border-radius: 2px;
transform: translateY(-200%);
transition: transform 0.15s;
}
@@ -165,8 +148,7 @@ body {
a:focus-visible,
button:focus-visible,
-input:focus-visible,
-.code-block:focus-visible {
+input:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
@@ -174,504 +156,518 @@ input:focus-visible,
a {
color: var(--link);
text-decoration: underline;
- text-decoration-thickness: 1px;
- text-underline-offset: 0.15em;
+ text-underline-offset: 0.12em;
}
-a:hover {
- color: var(--link-hover);
-}
+a:hover { color: var(--link-hover); }
-.rfc p a,
-.spec-table a,
-.endpoint a {
- text-decoration-thickness: 1.5px;
-}
-
-.toc nav a,
-.contact-card,
+.toc a,
+.meta-table a,
.back,
.theme-option span {
text-decoration: none;
}
-@media (max-width: 800px) {
- body { grid-template-columns: 1fr; }
- .toc {
- position: static !important;
- height: auto !important;
- border-bottom: 1px solid var(--rule);
- padding: 1rem 1.5rem !important;
- }
- .toc nav { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; }
- .toc .meta { display: none; }
+.contract-table a {
+ text-decoration: underline;
}
+/* --- sidebar --- */
.toc {
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
- padding: 2rem 1.25rem;
+ padding: 1.5rem 1rem 1.75rem;
+ background: var(--paper);
border-right: 1px solid var(--rule);
font-family: var(--mono);
- font-size: 0.7rem;
+ font-size: 0.65rem;
display: flex;
flex-direction: column;
- gap: 0.5rem;
}
.back {
- display: block;
color: var(--muted);
- text-decoration: none;
margin-bottom: 0.5rem;
- min-height: 2.75rem;
- line-height: 2.75rem;
+ line-height: 1.8;
}
.back:hover { color: var(--link); }
-.toc-title {
- font-size: 0.65rem;
- font-weight: 500;
+.toc-doc-id {
letter-spacing: 0.12em;
- text-transform: uppercase;
color: var(--muted);
- margin-bottom: 0.25rem;
+ margin-bottom: 0.65rem;
}
-.toc nav {
- display: flex;
- flex-direction: column;
- gap: 0.15rem;
+.toc-title {
+ font-size: 0.6rem;
+ font-weight: 500;
+ letter-spacing: var(--heading-track);
+ text-transform: uppercase;
+ color: var(--muted);
+ margin-bottom: 0.45rem;
+ padding-bottom: 0.45rem;
+ border-bottom: 1px solid var(--rule);
}
.toc nav a {
- color: var(--ink);
- text-decoration: none;
- padding: 0.35rem 0.25rem;
- min-height: 2.75rem;
- display: flex;
+ display: grid;
+ grid-template-columns: 1.35rem 1fr;
+ gap: 0.4rem;
align-items: center;
- border-radius: 2px;
+ color: var(--ink);
+ padding: 0.35rem 0.3rem;
+ line-height: 1.35;
+ min-height: 2.25rem;
}
-.toc nav a:hover { color: var(--link); }
+.toc-num {
+ color: var(--muted);
+ text-align: right;
+}
+
+.toc nav a:hover,
+.toc nav a:hover .toc-num { color: var(--link); }
.toc nav a.is-active {
font-weight: 600;
- color: var(--ink);
background: var(--nav-active);
- padding: 0.35rem 0.5rem;
- box-shadow: inset 0 0 0 1px var(--rule);
+ outline: 1px solid var(--rule);
}
-/* preferences */
+.toc nav a.is-active .toc-num { color: var(--ink); }
+
.prefs {
margin-top: auto;
- padding-top: 1rem;
+ padding-top: 0.85rem;
border-top: 1px solid var(--rule);
}
.prefs-heading {
- font-size: 0.65rem;
- font-weight: 500;
- letter-spacing: 0.12em;
+ font-size: 0.58rem;
+ letter-spacing: var(--heading-track);
text-transform: uppercase;
color: var(--muted);
- margin-bottom: 0.6rem;
+ margin-bottom: 0.45rem;
}
-.theme-field {
- border: 0;
- margin: 0 0 0.75rem;
- padding: 0;
-}
-
-.theme-legend {
- font-size: 0.62rem;
- color: var(--muted);
- margin-bottom: 0.35rem;
-}
-
-.theme-options {
- display: flex;
- gap: 0.25rem;
-}
-
-.theme-option {
- flex: 1;
- cursor: pointer;
-}
-
-.theme-option input {
- position: absolute;
- opacity: 0;
- width: 0;
- height: 0;
-}
+.theme-field { border: 0; margin: 0 0 0.55rem; padding: 0; }
+.theme-legend { font-size: 0.58rem; color: var(--muted); margin-bottom: 0.25rem; }
+.theme-options { display: flex; gap: 0.15rem; }
+.theme-option { flex: 1; cursor: pointer; }
+.theme-option input { position: absolute; opacity: 0; width: 0; height: 0; }
.theme-option span {
display: flex;
align-items: center;
justify-content: center;
- min-height: 2.75rem;
- padding: 0.35rem 0.25rem;
+ min-height: 2.25rem;
border: 1px solid var(--rule);
- background: var(--surface);
+ background: var(--sheet);
color: var(--ink);
- text-align: center;
- border-radius: 2px;
- transition: background 0.12s, border-color 0.12s;
}
.theme-option input:checked + span {
- border-color: var(--nav-active-border);
- background: var(--nav-active);
font-weight: 600;
- color: var(--ink);
-}
-
-.theme-option input:focus-visible + span {
- outline: 3px solid var(--focus);
- outline-offset: 2px;
+ background: var(--nav-active);
+ outline: 1px solid var(--rule-strong);
}
.font-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
- gap: 0.35rem 0.5rem;
+ gap: 0.25rem 0.4rem;
}
-.font-label {
- width: 100%;
- font-size: 0.62rem;
- color: var(--muted);
-}
-
-.font-buttons {
- display: flex;
- gap: 0.25rem;
-}
+.font-label { width: 100%; font-size: 0.58rem; color: var(--muted); }
+.font-buttons { display: flex; gap: 0.15rem; }
.font-btn {
font-family: var(--mono);
- font-size: 0.75rem;
- min-width: 2.75rem;
- min-height: 2.75rem;
- padding: 0.35rem 0.5rem;
+ font-size: 0.68rem;
+ min-width: 2.25rem;
+ min-height: 2.25rem;
border: 1px solid var(--rule);
- background: var(--surface);
+ background: var(--sheet);
color: var(--ink);
cursor: pointer;
- border-radius: 2px;
}
-.font-btn:hover:not(:disabled) {
- background: var(--nav-active);
- border-color: var(--nav-active-border);
-}
-
-.font-btn:disabled {
- opacity: 0.45;
- cursor: not-allowed;
-}
-
-.font-scale-readout {
- font-size: 0.62rem;
- color: var(--muted);
- min-width: 2.5rem;
-}
+.font-btn:hover:not(:disabled) { background: var(--nav-active); }
+.font-btn:disabled { opacity: 0.45; cursor: not-allowed; }
+.font-scale-readout { font-size: 0.58rem; color: var(--muted); }
.meta {
- margin-top: 0.75rem;
+ margin-top: 0.55rem;
color: var(--muted);
line-height: 1.5;
+ font-size: 0.58rem;
}
+/* --- instrument (contract page) --- */
.main {
min-width: 0;
+ background: var(--desk);
}
-.rfc {
- max-width: 42rem;
- padding: 3rem 2.5rem 5rem;
+.desk {
+ padding: 2rem clamp(1rem, 3vw, 2.5rem) 3rem;
+ min-height: 100vh;
}
-@media (max-width: 600px) {
- .rfc { padding: 2rem 1.25rem 4rem; }
+.instrument {
+ max-width: var(--measure);
+ margin: 0 auto;
+ background: var(--sheet);
+ border: 1px solid var(--rule-strong);
+ box-shadow: var(--shadow);
}
-.rfc-header {
- margin-bottom: 2.5rem;
- padding-bottom: 1.5rem;
- border-bottom: 2px solid var(--ink);
+/* letterhead */
+.letterhead {
+ padding: 2.25rem var(--pad-x) 1.75rem;
+ text-align: center;
+ border-bottom: 1px solid var(--rule-strong);
}
-.category {
+.doc-class {
font-family: var(--mono);
- font-size: 0.7rem;
- letter-spacing: 0.1em;
+ font-size: 0.65rem;
+ letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
- margin-bottom: 0.5rem;
+ margin-bottom: 0.75rem;
}
-.rfc-header h1 {
- font-size: 1.75rem;
+.letterhead h1 {
+ font-size: 1.5rem;
font-weight: 600;
- line-height: 1.25;
- margin-bottom: 1.25rem;
+ line-height: 1.3;
+ letter-spacing: 0.01em;
+ text-transform: uppercase;
+ margin-bottom: 0.75rem;
}
-.rfc-meta {
- width: 100%;
- font-size: 0.85rem;
- border-collapse: collapse;
-}
-
-.rfc-meta th {
- text-align: left;
- font-family: var(--mono);
- font-weight: 500;
+.doc-lead {
+ font-size: 0.875rem;
color: var(--muted);
- padding: 0.25rem 1rem 0.25rem 0;
- width: 5rem;
+ line-height: 1.55;
+ max-width: 36rem;
+ margin: 0 auto 1.25rem;
+}
+
+.meta-table {
+ width: 100%;
+ max-width: 22rem;
+ margin: 0 auto;
+ border-collapse: collapse;
+ font-size: 0.8125rem;
+ text-align: left;
+}
+
+.meta-table th,
+.meta-table td {
+ border: 1px solid var(--rule);
+ padding: 0.4rem 0.65rem;
+ vertical-align: top;
+}
+
+.meta-table th {
+ width: 6.5rem;
+ font-family: var(--mono);
+ font-size: 0.6rem;
+ font-weight: 500;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--muted);
+ background: var(--panel);
+}
+
+.meta-table td {
+ font-family: var(--mono);
+ font-size: 0.75rem;
}
.badge {
font-family: var(--mono);
- font-size: 0.65rem;
- padding: 0.15rem 0.45rem;
+ font-size: 0.58rem;
+ padding: 0.1rem 0.35rem;
background: var(--badge-ok-bg);
color: var(--badge-ok-fg);
border: 1px solid var(--badge-ok-border);
- letter-spacing: 0.04em;
+ letter-spacing: 0.05em;
}
-.badge.muted {
- background: var(--panel);
- color: var(--muted);
- border-color: var(--rule);
+/* shared section rhythm */
+.preamble,
+.article {
+ padding: var(--pad-y) var(--pad-x);
+ border-top: 1px solid var(--rule);
}
-section {
- margin-bottom: 2.5rem;
- scroll-margin-top: 2rem;
+.clause-anchor {
+ scroll-margin-top: 1.25rem;
}
-section h2 {
- font-size: 1.15rem;
- font-weight: 600;
- margin-bottom: 0.75rem;
+.section-heading {
display: flex;
align-items: baseline;
- gap: 0.35rem;
-}
-
-.sec-num { font-family: var(--mono); font-weight: 500; color: var(--muted); }
-
-section p { margin-bottom: 0.75rem; color: var(--ink); }
-
-.code-block {
- background: var(--code-bg);
- border: 1px solid var(--rule);
- padding: 1rem 1.25rem;
- margin: 1rem 0;
- overflow-x: auto;
-}
-
-.code-block code {
+ gap: 0.5rem;
font-family: var(--mono);
- font-size: 0.8rem;
- line-height: 1.5;
- color: var(--code-fg);
-}
-
-.endpoint {
- border: 1px solid var(--rule);
- margin: 1.25rem 0;
- background: var(--surface);
-}
-
-.endpoint-head {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 0.5rem 0.75rem;
- padding: 0.75rem 1rem;
- background: var(--panel);
- border-bottom: 1px solid var(--rule);
- font-family: var(--mono);
- font-size: 0.8rem;
-}
-
-.method {
+ font-size: 0.75rem;
font-weight: 600;
- color: var(--badge-ok-fg);
- background: var(--badge-ok-bg);
- border: 1px solid var(--badge-ok-border);
- padding: 0.15rem 0.4rem;
- font-size: 0.7rem;
+ letter-spacing: var(--heading-track);
+ text-transform: uppercase;
+ color: var(--ink);
+ margin-bottom: 1rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--rule);
}
-.path { color: var(--path); font-weight: 500; }
-
-.ext { margin-left: auto; font-size: 0.75rem; }
-.ext a { color: var(--muted); }
-.ext a:hover { color: var(--link); }
-
-.endpoint > p,
-.endpoint > ul {
- padding: 0.75rem 1rem;
- font-size: 0.95rem;
-}
-
-.endpoint ul {
- padding-top: 0;
- padding-left: 2rem;
+.section-num {
+ flex-shrink: 0;
+ min-width: 1.75rem;
color: var(--muted);
- font-size: 0.85rem;
}
-.table-wrap {
- overflow-x: auto;
- margin-top: 0.75rem;
- -webkit-overflow-scrolling: touch;
+.preamble p,
+.article > p,
+.subsection p {
+ margin-bottom: 0.75rem;
}
-.spec-table {
+.preamble p:last-child,
+.article > p:last-of-type:not(.section-lead),
+.subsection p:last-child {
+ margin-bottom: 0;
+}
+
+.section-lead {
+ color: var(--muted);
+ font-size: 0.875rem;
+ margin-bottom: 1rem !important;
+}
+
+/* subsections: uniform 2.x / 3.x numbering */
+.subsections {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ border: 1px solid var(--rule);
+}
+
+.subsection {
+ padding: 0.85rem 1rem 0.85rem 1rem;
+ border-top: 1px solid var(--rule);
+ display: grid;
+ grid-template-columns: 2.5rem 1fr;
+ gap: 0.65rem 1rem;
+ align-items: start;
+}
+
+.subsections .subsection:first-child {
+ border-top: none;
+}
+
+.article[data-article="2"] .subsections {
+ counter-reset: sub;
+}
+
+.article[data-article="2"] .subsection {
+ counter-increment: sub;
+}
+
+.article[data-article="2"] .subsection::before {
+ content: "2." counter(sub);
+ font-family: var(--mono);
+ font-size: 0.65rem;
+ font-weight: 600;
+ color: var(--muted);
+ padding-top: 0.15rem;
+}
+
+.article[data-article="3"] .subsections {
+ counter-reset: sub;
+}
+
+.article[data-article="3"] .subsection {
+ counter-increment: sub;
+}
+
+.article[data-article="3"] .subsection::before {
+ content: "3." counter(sub);
+ font-family: var(--mono);
+ font-size: 0.65rem;
+ font-weight: 600;
+ color: var(--muted);
+ padding-top: 0.15rem;
+}
+
+.subsection-title {
+ font-family: var(--serif);
+ font-size: var(--body-size);
+ font-weight: 600;
+ line-height: 1.4;
+ margin: 0;
+ grid-column: 2;
+ grid-row: 1;
+}
+
+.subsection > p {
+ grid-column: 2;
+ grid-row: 2;
+ margin: 0;
+ font-size: 0.875rem;
+ color: var(--ink);
+}
+
+.subsection::before {
+ grid-column: 1;
+ grid-row: 1 / -1;
+}
+
+.xref {
+ font-size: 0.8125rem;
+ color: var(--muted);
+}
+
+.xref a {
+ font-family: var(--mono);
+ font-size: 0.75rem;
+}
+
+/* contract tables */
+.contract-table {
width: 100%;
- min-width: 20rem;
border-collapse: collapse;
- font-size: 0.9rem;
+ font-size: 0.875rem;
+ margin-top: 0.25rem;
}
-.spec-table th,
-.spec-table td {
+.contract-table th,
+.contract-table td {
border: 1px solid var(--rule);
padding: 0.5rem 0.75rem;
text-align: left;
+ vertical-align: top;
}
-.spec-table th {
+.contract-table thead th {
font-family: var(--mono);
- font-size: 0.75rem;
- background: var(--table-head);
- font-weight: 500;
-}
-
-.spec-table code {
- font-family: var(--mono);
- font-size: 0.8rem;
- color: var(--code-fg);
-}
-
-.spec-table a { color: var(--link); }
-
-.contact-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1rem;
- margin: 1.25rem 0;
-}
-
-@media (max-width: 500px) {
- .contact-grid { grid-template-columns: 1fr; }
-}
-
-.contact-card {
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
- padding: 1rem;
- border: 2px solid var(--ink);
- text-decoration: none;
- color: var(--ink);
- font-family: var(--mono);
- font-size: 0.85rem;
- transition: background 0.15s;
- min-height: 2.75rem;
-}
-
-.contact-card:hover,
-.contact-card:focus-visible {
+ font-size: 0.6rem;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--muted);
background: var(--panel);
}
-.method.post {
- font-weight: 600;
- color: var(--badge-post-fg);
- background: var(--badge-post-bg);
- border: 1px solid var(--badge-post-border);
- align-self: flex-start;
- padding: 0.1rem 0.35rem;
- font-size: 0.65rem;
-}
-
-.back-top {
- display: inline-block;
- margin-top: 1.5rem;
+.contract-table tbody th {
+ width: 9rem;
font-family: var(--mono);
- font-size: 0.75rem;
+ font-size: 0.6rem;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
color: var(--muted);
- text-decoration: none;
+ background: var(--panel);
}
-.back-top:hover {
- color: var(--link);
+.contract-table td {
+ font-size: 0.875rem;
}
-.contact-card .desc {
- font-family: var(--serif);
- font-size: 0.9rem;
- color: var(--muted);
+.contract-table--contact td a {
+ font-family: var(--mono);
+ font-size: 0.8125rem;
}
-.copyright {
- font-size: 0.8rem;
+/* final article */
+.article--final {
+ border-top: 3px double var(--rule-strong);
+}
+
+.instrument-footer {
+ margin-top: 1.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--rule);
+ font-family: var(--mono);
+ font-size: 0.65rem;
color: var(--muted);
- margin-top: 2rem;
+ line-height: 1.65;
+ text-align: center;
+}
+
+.instrument-footer p + p {
+ margin-top: 0.2rem;
+}
+
+/* responsive */
+@media (max-width: 860px) {
+ :root {
+ --pad-x: 1.5rem;
+ --pad-y: 1.35rem;
+ }
+
+ body { grid-template-columns: 1fr; }
+
+ .toc {
+ position: static;
+ height: auto;
+ border-right: none;
+ border-bottom: 1px solid var(--rule);
+ }
+
+ .toc nav {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.2rem;
+ }
+
+ .toc nav a {
+ min-height: auto;
+ padding: 0.3rem 0.5rem;
+ }
+
+ .meta { display: none; }
+
+ .desk { padding: 1rem 0.75rem 2rem; }
+
+ .subsection {
+ grid-template-columns: 2rem 1fr;
+ gap: 0.35rem 0.65rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .letterhead h1 { font-size: 1.2rem; }
+
+ .subsection {
+ grid-template-columns: 1fr;
+ padding-left: 0.85rem;
+ }
+
+ .subsection::before {
+ grid-column: 1;
+ grid-row: auto;
+ margin-bottom: 0.25rem;
+ }
+
+ .subsection-title,
+ .subsection > p {
+ grid-column: 1;
+ }
}
@media print {
.skip-link,
.prefs,
- .back {
- display: none !important;
- }
+ .back { display: none !important; }
- body {
- display: block;
- background: #fff;
- color: #000;
- }
-
- .toc {
- position: static;
- height: auto;
- border: none;
- padding: 0 0 1rem;
- }
-
- .toc nav a.is-active {
- font-weight: 700;
- box-shadow: none;
- }
-
- a {
- color: #000;
- text-decoration: underline;
- }
-
- .rfc {
- max-width: none;
- padding: 0;
- }
-
- section {
- break-inside: avoid;
- }
+ body { display: block; background: #fff; }
+ .desk { padding: 0; }
+ .instrument { box-shadow: none; border: none; max-width: none; }
}
diff --git a/spec/spec.js b/spec/spec.js
index 5cc100b..6988ad4 100644
--- a/spec/spec.js
+++ b/spec/spec.js
@@ -20,9 +20,9 @@ function fontIndexFromScale(scale) {
}
const THEME_COLORS = {
- light: '#f6f3ee',
- dim: '#c9c3b8',
- dark: '#10100f',
+ light: '#e8e4dc',
+ dim: '#b8b2a6',
+ dark: '#080807',
};
function applyTheme(theme) {
@@ -83,7 +83,7 @@ function initPreferences() {
}
function initScrollSpy() {
- const sections = document.querySelectorAll('section[id]');
+ const sections = document.querySelectorAll('.clause-anchor[id]');
const links = document.querySelectorAll('.toc nav a');
if (!sections.length || !links.length) return;
diff --git a/stack-folder/folder.css b/stack-folder/folder.css
index 4acae53..4d7cc66 100644
--- a/stack-folder/folder.css
+++ b/stack-folder/folder.css
@@ -60,14 +60,18 @@ body { font-family: var(--sans); background: #2a2824; color: #1a1814; }
padding: var(--stack-nav) 1rem 0;
}
-/* Whole folder sticks; tab staggered via folder top; bodies align to reveal */
+/* All folders stick at same line — higher z-index covers previous card */
.folder {
position: sticky;
+ top: var(--stack-reveal);
width: 100%;
- z-index: 10;
}
.tab {
+ position: absolute;
+ left: 0;
+ bottom: 100%;
+ margin-bottom: 0;
display: block;
width: fit-content;
max-width: calc(100% - 1rem);
@@ -90,40 +94,32 @@ body { font-family: var(--sans); background: #2a2824; color: #1a1814; }
.body {
background: #e8e2d4;
border: 1px solid #c4b8a8;
- border-top: none;
border-radius: 0 10px 10px 10px;
padding: 1.25rem 1.4rem 2rem;
min-height: var(--stack-body-h);
box-shadow: 0 10px 32px rgba(0,0,0,0.25);
}
-.f0 { top: calc(var(--stack-stick) + var(--stack-step) * 0); }
-.f0 .tab { margin-left: calc(var(--tab-offset) * 0); background: #c9a86c; color: #2a2824; }
-.f0 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-tab-h)); }
+.f0 { z-index: 10; }
+.f0 .tab { margin-left: calc(var(--tab-offset) * 0); bottom: calc(100% + var(--stack-step) * 0); background: #c9a86c; color: #2a2824; }
-.f1 { top: calc(var(--stack-stick) + var(--stack-step) * 1); }
-.f1 .tab { margin-left: calc(var(--tab-offset) * 1); background: #a8c4d4; color: #1a2830; }
-.f1 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 1 - var(--stack-tab-h)); }
+.f1 { z-index: 11; }
+.f1 .tab { margin-left: calc(var(--tab-offset) * 1); bottom: calc(100% + var(--stack-step) * 1); background: #a8c4d4; color: #1a2830; }
-.f2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
-.f2 .tab { margin-left: calc(var(--tab-offset) * 2); background: #b8d4a8; color: #1a2818; }
-.f2 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 2 - var(--stack-tab-h)); }
+.f2 { z-index: 12; }
+.f2 .tab { margin-left: calc(var(--tab-offset) * 2); bottom: calc(100% + var(--stack-step) * 2); background: #b8d4a8; color: #1a2818; }
-.f3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
-.f3 .tab { margin-left: calc(var(--tab-offset) * 3); background: #d4b8c4; color: #2a1820; }
-.f3 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 3 - var(--stack-tab-h)); }
+.f3 { z-index: 13; }
+.f3 .tab { margin-left: calc(var(--tab-offset) * 3); bottom: calc(100% + var(--stack-step) * 3); background: #d4b8c4; color: #2a1820; }
-.f4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); }
-.f4 .tab { margin-left: calc(var(--tab-offset) * 4); background: #d4c8a8; color: #2a2418; }
-.f4 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 4 - var(--stack-tab-h)); }
+.f4 { z-index: 14; }
+.f4 .tab { margin-left: calc(var(--tab-offset) * 4); bottom: calc(100% + var(--stack-step) * 4); background: #d4c8a8; color: #2a2418; }
-.f5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
-.f5 .tab { margin-left: calc(var(--tab-offset) * 5); background: #c4c4c4; color: #2a2a2a; }
-.f5 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 5 - var(--stack-tab-h)); }
+.f5 { z-index: 15; }
+.f5 .tab { margin-left: calc(var(--tab-offset) * 5); bottom: calc(100% + var(--stack-step) * 5); background: #c4c4c4; color: #2a2a2a; }
-.f6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); }
-.f6 .tab { margin-left: calc(var(--tab-offset) * 6); background: #2a4a6b; color: #e8e2d4; }
-.f6 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 6 - var(--stack-tab-h)); }
+.f6 { z-index: 16; }
+.f6 .tab { margin-left: calc(var(--tab-offset) * 6); bottom: calc(100% + var(--stack-step) * 6); background: #2a4a6b; color: #e8e2d4; }
.body h1 { font-size: 1.65rem; margin-bottom: 0.35rem; }
.body h2 { font-size: 1.25rem; margin-bottom: 0.4rem; }
diff --git a/stack-rack/rack.css b/stack-rack/rack.css
index d298624..a5f748e 100644
--- a/stack-rack/rack.css
+++ b/stack-rack/rack.css
@@ -38,19 +38,25 @@ body {
.unit {
position: sticky;
+ top: var(--stack-reveal);
margin: 0;
border: 1px solid #2a3448;
background: #161a22;
border-radius: 2px;
box-shadow: 0 6px 0 #0a0c10, 0 12px 24px rgba(0,0,0,0.4);
- z-index: 10;
}
.unit-head {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: calc(100% + var(--stack-step) * var(--layer, 0));
display: flex; align-items: center; gap: 0.5rem;
padding: 0.4rem 0.65rem;
background: #1a2030;
- border-bottom: 1px solid #2a3448;
+ border: 1px solid #2a3448;
+ border-bottom: none;
+ border-radius: 2px 2px 0 0;
}
.unit-head button.jump {
@@ -76,26 +82,13 @@ body {
background: #161a22;
}
-.u0 { top: calc(var(--stack-stick) + var(--stack-step) * 0); }
-.u0 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-tab-h)); }
-
-.u1 { top: calc(var(--stack-stick) + var(--stack-step) * 1); }
-.u1 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 1 - var(--stack-tab-h)); }
-
-.u2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
-.u2 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 2 - var(--stack-tab-h)); }
-
-.u3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
-.u3 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 3 - var(--stack-tab-h)); }
-
-.u4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); }
-.u4 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 4 - var(--stack-tab-h)); }
-
-.u5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
-.u5 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 5 - var(--stack-tab-h)); }
-
-.u6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); }
-.u6 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 6 - var(--stack-tab-h)); }
+.u0 { z-index: 10; --layer: 0; }
+.u1 { z-index: 11; --layer: 1; }
+.u2 { z-index: 12; --layer: 2; }
+.u3 { z-index: 13; --layer: 3; }
+.u4 { z-index: 14; --layer: 4; }
+.u5 { z-index: 15; --layer: 5; }
+.u6 { z-index: 16; --layer: 6; }
.unit-body strong { color: #e5e7eb; display: block; margin-bottom: 0.2rem; }
.unit-body p { color: #6b7280; font-size: 0.7rem; }
diff --git a/stack-trace/trace.css b/stack-trace/trace.css
index 0768efd..412946b 100644
--- a/stack-trace/trace.css
+++ b/stack-trace/trace.css
@@ -28,14 +28,18 @@ body {
.frame {
position: sticky;
+ top: var(--stack-reveal);
margin: 0 0.5rem;
border-left: 3px solid #3a3a44;
background: #141418;
box-shadow: 0 8px 0 #0a0a0c, 0 14px 28px rgba(0,0,0,0.45);
- z-index: 10;
}
.frame-line {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: calc(100% + var(--stack-step) * var(--layer, 0));
display: block;
font-size: 0.66rem;
color: #6b9b6b;
@@ -56,26 +60,13 @@ body {
background: #141418;
}
-.f0 { top: calc(var(--stack-stick) + var(--stack-step) * 0); border-color: #c4a574; }
-.f0 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-tab-h)); }
-
-.f1 { top: calc(var(--stack-stick) + var(--stack-step) * 1); }
-.f1 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 1 - var(--stack-tab-h)); }
-
-.f2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
-.f2 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 2 - var(--stack-tab-h)); }
-
-.f3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
-.f3 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 3 - var(--stack-tab-h)); }
-
-.f4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); border-color: #6b8b9b; }
-.f4 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 4 - var(--stack-tab-h)); }
-
-.f5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
-.f5 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 5 - var(--stack-tab-h)); }
-
-.f6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); border-color: #7eb87a; }
-.f6 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 6 - var(--stack-tab-h)); }
+.f0 { z-index: 10; border-color: #c4a574; --layer: 0; }
+.f1 { z-index: 11; --layer: 1; }
+.f2 { z-index: 12; --layer: 2; }
+.f3 { z-index: 13; --layer: 3; }
+.f4 { z-index: 14; border-color: #6b8b9b; --layer: 4; }
+.f5 { z-index: 15; --layer: 5; }
+.f6 { z-index: 16; border-color: #7eb87a; --layer: 6; }
.frame-body strong { color: #e8e6e3; font-weight: 500; display: block; margin-bottom: 0.2rem; }
.frame-body p { color: #6b6966; font-size: 0.74rem; }
diff --git a/stack/index.html b/stack/index.html
index def0e5f..9d41c74 100644
--- a/stack/index.html
+++ b/stack/index.html
@@ -23,7 +23,7 @@
Levkin
Software · Canada · remote
- Boutique engineering — production systems, automation, enterprise.
+ Boutique engineering — production systems, automation, enterprise.
15+ yrs 8h→2m 24/7
Taking new engagements
@@ -31,37 +31,37 @@