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 @@
-
-
-

Request for Comments

-

Levkin Software Development Company

- - - - - - - -
StatusACTIVE
EntityLevkin Inc.
Domainlevkin.ca
Updated
-
+
+
+
+

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.

-
- -
-

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";
-}
-
- -
-

Services

-

The following endpoints represent primary service offerings. Each maps to a deployable capability.

- -
-
- - /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
  • -
-
- -
-
- - /automation - auto.levkin.ca (external) -
-

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 - caseware.levkin.ca (external) -
-

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
  • -
-
- -
-
- - /quality-engineering - iliadobkin.com (external) -
-

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)
  • -
-
-
- -
-

Documented Outcomes

-
- - - - - +
Documented outcomes metrics
MetricValueContext
- - - - + + + +
release_time8h → <2minCaseWare template pipeline rebuild
experience15+ yearsCaseWare, automation, enterprise CI/CD
automation_uptime24/7Pipelines monitored, not happy-path demos
engagement_modelFixed scopeQuoted per project after discovery — no hourly surprises
DocumentLK-SPEC-1.0
EntityLevkin Inc.
StatusACTIVE
Effective
-
-

Engagement flow: Discover (15 min) → Design (proposal) → Ship (tested, documented) → Maintain (optional).

-
+
-
-

Required Properties

-

All Levkin deliverables MUST satisfy the following constraints unless explicitly waived in writing.

-
- - +
+

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.

+
+
+
+ +
+ +
Required deliverable properties
+ - + - - - - + + +
External references
PropertyRequirementRationale
ReferencePurpose
reliabilityRetries, alerts, graceful degradationProduction ≠ demo
documentationRunbook or README sufficient for handoffBus factor > 1
testabilityAutomated tests before live dataRegressions are expensive
pragmatismSmallest solution that solves the problem20-line script > 200-node workflow
auto.levkin.ca (external)Automation
caseware.levkin.ca (external)CaseWare
cal.levkin.ca (external)Book a discovery call
-
-
+ -
-

Registered Subdomains

-
- - - - - +
+

5. Contact

+

To discuss a project, use either channel below. A formal statement of work follows acceptance of a written proposal.

+
Registered Levkin subdomains
HostPurposeStatus
+ - - - - - - + + + + + + + +
Contact channels
auto.levkin.ca (external)Business automationlive
caseware.levkin.ca (external)CaseWare consultinglive
jobs.levkin.ca (external)Job orchestration (internal)auth
git.levkin.ca (external)Source controllive
iliadobkin.com (external)SDET portfolio · quality engineeringlive
cal.levkin.ca (external)Scheduling · 15 min consultationlive
Schedule15-minute discovery call (opens cal.levkin.ca)
Correspondenceilia@levkine.ca
-
-
- -
-

Contact

-

To initiate an engagement, send a POST to one of the following channels:

- - - ↑ Back to top -
-
+ + +
+
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 @@
foundation

Levkin

Software · Canada · remote

-

Boutique engineering — production systems, automation, enterprise.

+

Boutique engineering — production systems, automation, enterprise.

15+ yrs8h→2m24/7

Taking new engagements

@@ -31,37 +31,37 @@
application

Custom software

-

Web apps, APIs, internal tools — TS · Python · .NET

+

Web apps, APIs, internal tools — TS · Python · .NET

automationauto.levkin.ca ↗

Automation

-

n8n · Zapier · CI/CD · webhooks · LLMs

+

n8n · Zapier · CI/CD · webhooks · LLMs

enterprisecaseware.levkin.ca ↗

CaseWare & CaseView

-

15+ years · CaseWare Intl, MNP, JazzIt

+

15+ years · CaseWare Intl, MNP, JazzIt

qualityiliadobkin.com ↗

Quality engineering

-

Senior SDET · test automation · CI/CD · trace-driven QA

+

Senior SDET · test automation · CI/CD · trace-driven QA

operationsjobs.levkin.ca ↗

Job Ops

-

Internal hiring orchestrator (auth required)

+

Internal hiring orchestrator (auth required)

interface

Engage

-

Discover → Proposal → Ship → Maintain

+

Discover → Proposal → Ship → Maintain

Book 15 min hello@levkine.ca diff --git a/stack/stack.css b/stack/stack.css index b6a718f..7bc4ba6 100644 --- a/stack/stack.css +++ b/stack/stack.css @@ -4,7 +4,6 @@ :root { --mono: 'IBM Plex Mono', ui-monospace, monospace; --sans: 'Instrument Sans', system-ui, sans-serif; - --card-tab-h: 0.5rem; } * { box-sizing: border-box; margin: 0; padding: 0; } @@ -37,16 +36,19 @@ body { .layer { position: sticky; + top: var(--stack-reveal); margin: 0; border-radius: 8px 8px 6px 6px; border: 1px solid rgba(255,255,255,0.08); box-shadow: 0 12px 36px rgba(0,0,0,0.5); - z-index: 10; } .layer-tab { + position: absolute; + left: 10px; + right: 10px; + bottom: calc(100% + var(--stack-step) * var(--layer, 0)); height: 6px; - margin: 0 10px -6px; border-radius: 5px 5px 0 0; background: inherit; filter: brightness(1.12); @@ -60,26 +62,13 @@ body { min-height: var(--stack-body-h); } -.layer-0 { background: #1c1c20; top: calc(var(--stack-stick) + var(--stack-step) * 0); } -.layer-0 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--card-tab-h)); } - -.layer-1 { background: #24242c; top: calc(var(--stack-stick) + var(--stack-step) * 1); } -.layer-1 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 1 - var(--card-tab-h)); } - -.layer-2 { background: #2c2c36; top: calc(var(--stack-stick) + var(--stack-step) * 2); } -.layer-2 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 2 - var(--card-tab-h)); } - -.layer-3 { background: #343440; top: calc(var(--stack-stick) + var(--stack-step) * 3); } -.layer-3 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 3 - var(--card-tab-h)); } - -.layer-4 { background: #3c3c4a; top: calc(var(--stack-stick) + var(--stack-step) * 4); } -.layer-4 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 4 - var(--card-tab-h)); } - -.layer-5 { background: #444454; top: calc(var(--stack-stick) + var(--stack-step) * 5); } -.layer-5 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 5 - var(--card-tab-h)); } - -.layer-6 { background: #4c4c5e; top: calc(var(--stack-stick) + var(--stack-step) * 6); } -.layer-6 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 6 - var(--card-tab-h)); } +.layer-0 { background: #1c1c20; z-index: 10; --layer: 0; } +.layer-1 { background: #24242c; z-index: 11; --layer: 1; } +.layer-2 { background: #2c2c36; z-index: 12; --layer: 2; } +.layer-3 { background: #343440; z-index: 13; --layer: 3; } +.layer-4 { background: #3c3c4a; z-index: 14; --layer: 4; } +.layer-5 { background: #444454; z-index: 15; --layer: 5; } +.layer-6 { background: #4c4c5e; z-index: 16; --layer: 6; } .layer-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.65rem; @@ -98,7 +87,7 @@ body { .layer h1 { font-size: 2rem; font-weight: 600; letter-spacing: -0.03em; } .layer h2 { font-size: 1.2rem; font-weight: 600; margin-bottom: 0.35rem; } .tagline { font-family: var(--mono); font-size: 0.65rem; color: #6b6966; margin-bottom: 0.4rem; } -.body { font-size: 0.9rem; color: #a8a6a1; } +.layer-copy { font-size: 0.9rem; color: #a8a6a1; } .chips { display: flex; flex-wrap: wrap; gap: 0.3rem; margin: 0.5rem 0; } .chips span { font-family: var(--mono); font-size: 0.55rem; padding: 0.15rem 0.4rem;