Fix stack cover: shared sticky line, z-index, show body only on stack.
All folders stick at --stack-reveal; higher layers paint over lower ones. Bodies hidden until panel aligns on the stack line (no vertical list). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cfe1cf3922
commit
7b54b47443
@ -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);
|
||||
|
||||
50
scripts/test-stack-cover.mjs
Normal file
50
scripts/test-stack-cover.mjs
Normal file
@ -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();
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
271
spec/index.html
271
spec/index.html
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Levkin — Company Specification</title>
|
||||
<meta name="description" content="Levkin: software development company. Specification v1.0." />
|
||||
<meta name="description" content="Levkin Inc.: software development and automation for production systems." />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
@ -25,15 +25,15 @@
|
||||
|
||||
<aside class="toc" aria-labelledby="toc-heading">
|
||||
<a href="/" class="back">← all options</a>
|
||||
<h2 id="toc-heading" class="toc-title">Contents</h2>
|
||||
<p class="toc-doc-id">LK-SPEC-1.0</p>
|
||||
<h2 id="toc-heading" class="toc-title">Table of contents</h2>
|
||||
<nav aria-label="Specification sections">
|
||||
<a href="#abstract">Abstract</a>
|
||||
<a href="#scope">1. Scope</a>
|
||||
<a href="#services">2. Services</a>
|
||||
<a href="#proof">3. Proof</a>
|
||||
<a href="#properties">4. Properties</a>
|
||||
<a href="#subsidiaries">5. Subdomains</a>
|
||||
<a href="#contact">6. Contact</a>
|
||||
<a href="#preamble"><span class="toc-num">—</span> Preamble</a>
|
||||
<a href="#scope"><span class="toc-num">1</span> Scope</a>
|
||||
<a href="#services"><span class="toc-num">2</span> Services</a>
|
||||
<a href="#working"><span class="toc-num">3</span> Terms of engagement</a>
|
||||
<a href="#links"><span class="toc-num">4</span> References</a>
|
||||
<a href="#contact"><span class="toc-num">5</span> Contact</a>
|
||||
</nav>
|
||||
|
||||
<section class="prefs" aria-labelledby="prefs-heading">
|
||||
@ -66,177 +66,122 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="meta">Levkin-Company-Spec<br />Version 1.0<br />May 2026</p>
|
||||
<p class="meta">Levkin Inc.<br />Effective May 2026</p>
|
||||
</aside>
|
||||
|
||||
<main id="main" class="main" tabindex="-1">
|
||||
<article class="rfc">
|
||||
<header class="rfc-header">
|
||||
<p class="category">Request for Comments</p>
|
||||
<h1>Levkin Software Development Company</h1>
|
||||
<table class="rfc-meta">
|
||||
<tbody>
|
||||
<tr><th scope="row">Status</th><td><span class="badge">ACTIVE</span></td></tr>
|
||||
<tr><th scope="row">Entity</th><td>Levkin Inc.</td></tr>
|
||||
<tr><th scope="row">Domain</th><td>levkin.ca</td></tr>
|
||||
<tr><th scope="row">Updated</th><td><time datetime="2026-05-20">2026-05-20</time></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</header>
|
||||
<div class="desk">
|
||||
<article class="instrument">
|
||||
<header class="letterhead">
|
||||
<p class="doc-class">Company Specification</p>
|
||||
<h1>Levkin Software Development Company</h1>
|
||||
<p class="doc-lead">Master overview of services and engagement terms. This document is not a binding agreement; a statement of work is issued for each engagement.</p>
|
||||
|
||||
<section id="abstract" aria-labelledby="abstract-heading">
|
||||
<h2 id="abstract-heading">Abstract</h2>
|
||||
<p>This document describes <strong>Levkin</strong>, 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.</p>
|
||||
<p>Quality engineering and SDET work are documented at <a href="https://iliadobkin.com" rel="noopener noreferrer">iliadobkin.com<span class="sr-only"> (external)</span></a> — an interactive portfolio (Playwright-style test runner UI, career timeline, trace-driven debugging showcase).</p>
|
||||
<p><span class="badge">AVAILABLE</span> Currently taking on new engagements.</p>
|
||||
</section>
|
||||
|
||||
<section id="scope" aria-labelledby="scope-heading">
|
||||
<h2 id="scope-heading"><span class="sec-num" aria-hidden="true">1.</span> Scope</h2>
|
||||
<p>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.</p>
|
||||
<pre class="code-block" tabindex="0"><code>interface Engagement {
|
||||
discovery: "15min call, no obligation"; // cal.levkin.ca/ilia/consult
|
||||
delivery: "production-ready, documented";
|
||||
maintenance: "optional, not mandatory lock-in";
|
||||
}</code></pre>
|
||||
</section>
|
||||
|
||||
<section id="services" aria-labelledby="services-heading">
|
||||
<h2 id="services-heading"><span class="sec-num" aria-hidden="true">2.</span> Services</h2>
|
||||
<p>The following endpoints represent primary service offerings. Each maps to a deployable capability.</p>
|
||||
|
||||
<article class="endpoint" aria-labelledby="ep-custom">
|
||||
<div class="endpoint-head" id="ep-custom">
|
||||
<span class="method" aria-hidden="true">BUILD</span>
|
||||
<span class="path">/custom-software</span>
|
||||
</div>
|
||||
<p>Web applications, APIs, internal tools. Stack-agnostic; preference for boring, proven technology.</p>
|
||||
<ul>
|
||||
<li>TypeScript / Node · Python · C# / .NET</li>
|
||||
<li>PostgreSQL · SQL Server · SQLite</li>
|
||||
<li>React · Vue · server-rendered where appropriate</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="endpoint" aria-labelledby="ep-auto">
|
||||
<div class="endpoint-head" id="ep-auto">
|
||||
<span class="method" aria-hidden="true">BUILD</span>
|
||||
<span class="path">/automation</span>
|
||||
<span class="ext"><a href="https://auto.levkin.ca" rel="noopener noreferrer">auto.levkin.ca<span class="sr-only"> (external)</span></a></span>
|
||||
</div>
|
||||
<p>Production-ready automation — scripts, no-code workflows, CI/CD, webhooks, AI integrations. Runs while you sleep.</p>
|
||||
<ul>
|
||||
<li>n8n · Zapier · Make · GitHub Actions · Jenkins · Azure DevOps</li>
|
||||
<li>Python · Node · Bash · macOS/iOS Shortcuts</li>
|
||||
<li>OpenAI · Claude · custom LLM pipelines</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="endpoint" aria-labelledby="ep-caseware">
|
||||
<div class="endpoint-head" id="ep-caseware">
|
||||
<span class="method" aria-hidden="true">BUILD</span>
|
||||
<span class="path">/caseware</span>
|
||||
<span class="ext"><a href="https://caseware.levkin.ca" rel="noopener noreferrer">caseware.levkin.ca<span class="sr-only"> (external)</span></a></span>
|
||||
</div>
|
||||
<p>CaseWare & CaseView features, client templates, release automation. 15+ years; teams at CaseWare International, MNP, JazzIt.</p>
|
||||
<ul>
|
||||
<li>C# · .NET · SQL Server · JavaScript automation</li>
|
||||
<li>Template delivery · CI/CD · mentorship · modernization</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="endpoint" aria-labelledby="ep-qe">
|
||||
<div class="endpoint-head" id="ep-qe">
|
||||
<span class="method" aria-hidden="true">BUILD</span>
|
||||
<span class="path">/quality-engineering</span>
|
||||
<span class="ext"><a href="https://iliadobkin.com" rel="noopener noreferrer">iliadobkin.com<span class="sr-only"> (external)</span></a></span>
|
||||
</div>
|
||||
<p>Senior SDET services — test automation, CI/CD pipelines, trace-driven debugging, contract QA leadership.</p>
|
||||
<ul>
|
||||
<li>Portfolio: <a href="https://iliadobkin.com" rel="noopener noreferrer">iliadobkin.com<span class="sr-only"> (external)</span></a> — runnable career specs, trace viewer</li>
|
||||
<li>Remote (ET) · Canadian · git.levkin.ca for source</li>
|
||||
<li>Playwright / JS automation · enterprise CI (Jenkins, Azure DevOps, GitHub Actions)</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section id="proof" aria-labelledby="proof-heading">
|
||||
<h2 id="proof-heading"><span class="sec-num" aria-hidden="true">3.</span> Documented Outcomes</h2>
|
||||
<div class="table-wrap">
|
||||
<table class="spec-table">
|
||||
<caption class="sr-only">Documented outcomes metrics</caption>
|
||||
<thead>
|
||||
<tr><th scope="col">Metric</th><th scope="col">Value</th><th scope="col">Context</th></tr>
|
||||
</thead>
|
||||
<table class="meta-table">
|
||||
<tbody>
|
||||
<tr><td><code>release_time</code></td><td>8h → <2min</td><td>CaseWare template pipeline rebuild</td></tr>
|
||||
<tr><td><code>experience</code></td><td>15+ years</td><td>CaseWare, automation, enterprise CI/CD</td></tr>
|
||||
<tr><td><code>automation_uptime</code></td><td>24/7</td><td>Pipelines monitored, not happy-path demos</td></tr>
|
||||
<tr><td><code>engagement_model</code></td><td>Fixed scope</td><td>Quoted per project after discovery — no hourly surprises</td></tr>
|
||||
<tr><th scope="row">Document</th><td>LK-SPEC-1.0</td></tr>
|
||||
<tr><th scope="row">Entity</th><td>Levkin Inc.</td></tr>
|
||||
<tr><th scope="row">Status</th><td><span class="badge">ACTIVE</span></td></tr>
|
||||
<tr><th scope="row">Effective</th><td><time datetime="2026-05-20">20 May 2026</time></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>Engagement flow: <strong>Discover</strong> (15 min) → <strong>Design</strong> (proposal) → <strong>Ship</strong> (tested, documented) → <strong>Maintain</strong> (optional).</p>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<section id="properties" aria-labelledby="properties-heading">
|
||||
<h2 id="properties-heading"><span class="sec-num" aria-hidden="true">4.</span> Required Properties</h2>
|
||||
<p>All Levkin deliverables MUST satisfy the following constraints unless explicitly waived in writing.</p>
|
||||
<div class="table-wrap">
|
||||
<table class="spec-table">
|
||||
<caption class="sr-only">Required deliverable properties</caption>
|
||||
<div id="preamble" class="preamble clause-anchor">
|
||||
<h2 class="section-heading"><span class="section-num">—</span> Preamble</h2>
|
||||
<p><strong>Levkin Inc.</strong> 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.</p>
|
||||
<p><span class="badge">AVAILABLE</span> Taking on new engagements. Remote across North American and European time zones.</p>
|
||||
</div>
|
||||
|
||||
<section id="scope" class="article clause-anchor" aria-labelledby="scope-heading">
|
||||
<h2 id="scope-heading" class="section-heading"><span class="section-num">1.</span> Scope</h2>
|
||||
<p>Boutique practice — not a staffing agency. Projects have a clear start, deliverable, and handoff so your team can own what ships.</p>
|
||||
<p>Typical flow: a short discovery call, a fixed-scope proposal, delivery with documentation, then optional ongoing support if you want it.</p>
|
||||
</section>
|
||||
|
||||
<section id="services" class="article clause-anchor" data-article="2" aria-labelledby="services-heading">
|
||||
<h2 id="services-heading" class="section-heading"><span class="section-num">2.</span> Services</h2>
|
||||
<p class="section-lead">Primary service lines. Scope, timeline, and fees for a given engagement are set out in a separate statement of work.</p>
|
||||
<div class="subsections">
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Custom software</h3>
|
||||
<p>Web apps, APIs, and internal tools — chosen to fit the problem, not a preset stack.</p>
|
||||
</div>
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Automation</h3>
|
||||
<p>Workflows and integrations that run reliably in the background — reporting, notifications, data sync, and repetitive ops work off your plate. <span class="xref">See <a href="https://auto.levkin.ca" rel="noopener noreferrer">auto.levkin.ca<span class="sr-only"> (external)</span></a>.</span></p>
|
||||
</div>
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">CaseWare</h3>
|
||||
<p>Templates, releases, and practice customization for firms on CaseWare — from small fixes to full pipeline overhauls. <span class="xref">See <a href="https://caseware.levkin.ca" rel="noopener noreferrer">caseware.levkin.ca<span class="sr-only"> (external)</span></a>.</span></p>
|
||||
</div>
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Quality & testing</h3>
|
||||
<p>Test strategy, automation, and release confidence for teams that need an experienced QA lead without hiring full-time. <span class="xref">See <a href="https://iliadobkin.com" rel="noopener noreferrer">iliadobkin.com<span class="sr-only"> (external)</span></a>.</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="working" class="article clause-anchor" data-article="3" aria-labelledby="working-heading">
|
||||
<h2 id="working-heading" class="section-heading"><span class="section-num">3.</span> Terms of engagement</h2>
|
||||
<div class="subsections">
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Fixed scope</h3>
|
||||
<p>Quoted after discovery; no open-ended hourly surprises.</p>
|
||||
</div>
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Production-ready</h3>
|
||||
<p>Monitoring, failure handling, and documentation included by default.</p>
|
||||
</div>
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Handoff-friendly</h3>
|
||||
<p>Your team or the next vendor can maintain what we ship.</p>
|
||||
</div>
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Right-sized</h3>
|
||||
<p>The smallest solution that actually solves the problem.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="links" class="article clause-anchor" aria-labelledby="links-heading">
|
||||
<h2 id="links-heading" class="section-heading"><span class="section-num">4.</span> References</h2>
|
||||
<table class="contract-table">
|
||||
<caption class="sr-only">External references</caption>
|
||||
<thead>
|
||||
<tr><th scope="col">Property</th><th scope="col">Requirement</th><th scope="col">Rationale</th></tr>
|
||||
<tr><th scope="col">Reference</th><th scope="col">Purpose</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>reliability</code></td><td>Retries, alerts, graceful degradation</td><td>Production ≠ demo</td></tr>
|
||||
<tr><td><code>documentation</code></td><td>Runbook or README sufficient for handoff</td><td>Bus factor > 1</td></tr>
|
||||
<tr><td><code>testability</code></td><td>Automated tests before live data</td><td>Regressions are expensive</td></tr>
|
||||
<tr><td><code>pragmatism</code></td><td>Smallest solution that solves the problem</td><td>20-line script > 200-node workflow</td></tr>
|
||||
<tr><td><a href="https://auto.levkin.ca" rel="noopener noreferrer">auto.levkin.ca<span class="sr-only"> (external)</span></a></td><td>Automation</td></tr>
|
||||
<tr><td><a href="https://caseware.levkin.ca" rel="noopener noreferrer">caseware.levkin.ca<span class="sr-only"> (external)</span></a></td><td>CaseWare</td></tr>
|
||||
<tr><td><a href="https://cal.levkin.ca/ilia/consult" rel="noopener noreferrer">cal.levkin.ca<span class="sr-only"> (external)</span></a></td><td>Book a discovery call</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="subsidiaries" aria-labelledby="subsidiaries-heading">
|
||||
<h2 id="subsidiaries-heading"><span class="sec-num" aria-hidden="true">5.</span> Registered Subdomains</h2>
|
||||
<div class="table-wrap">
|
||||
<table class="spec-table subdomains">
|
||||
<caption class="sr-only">Registered Levkin subdomains</caption>
|
||||
<thead>
|
||||
<tr><th scope="col">Host</th><th scope="col">Purpose</th><th scope="col">Status</th></tr>
|
||||
</thead>
|
||||
<section id="contact" class="article article--final clause-anchor" aria-labelledby="contact-heading">
|
||||
<h2 id="contact-heading" class="section-heading"><span class="section-num">5.</span> Contact</h2>
|
||||
<p class="section-lead">To discuss a project, use either channel below. A formal statement of work follows acceptance of a written proposal.</p>
|
||||
<table class="contract-table contract-table--contact">
|
||||
<caption class="sr-only">Contact channels</caption>
|
||||
<tbody>
|
||||
<tr><td><a href="https://auto.levkin.ca" rel="noopener noreferrer">auto.levkin.ca<span class="sr-only"> (external)</span></a></td><td>Business automation</td><td><span class="badge">live</span></td></tr>
|
||||
<tr><td><a href="https://caseware.levkin.ca" rel="noopener noreferrer">caseware.levkin.ca<span class="sr-only"> (external)</span></a></td><td>CaseWare consulting</td><td><span class="badge">live</span></td></tr>
|
||||
<tr><td><a href="https://jobs.levkin.ca" rel="noopener noreferrer">jobs.levkin.ca<span class="sr-only"> (external)</span></a></td><td>Job orchestration (internal)</td><td><span class="badge muted">auth</span></td></tr>
|
||||
<tr><td><a href="https://git.levkin.ca" rel="noopener noreferrer">git.levkin.ca<span class="sr-only"> (external)</span></a></td><td>Source control</td><td><span class="badge">live</span></td></tr>
|
||||
<tr><td><a href="https://iliadobkin.com" rel="noopener noreferrer">iliadobkin.com<span class="sr-only"> (external)</span></a></td><td>SDET portfolio · quality engineering</td><td><span class="badge">live</span></td></tr>
|
||||
<tr><td><a href="https://cal.levkin.ca/ilia/consult" rel="noopener noreferrer">cal.levkin.ca<span class="sr-only"> (external)</span></a></td><td>Scheduling · 15 min consultation</td><td><span class="badge">live</span></td></tr>
|
||||
<tr>
|
||||
<th scope="row">Schedule</th>
|
||||
<td><a href="https://cal.levkin.ca/ilia/consult" rel="noopener noreferrer">15-minute discovery call<span class="sr-only"> (opens cal.levkin.ca)</span></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Correspondence</th>
|
||||
<td><a href="mailto:ilia@levkine.ca?subject=Project%20enquiry">ilia@levkine.ca</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="contact" class="contact" aria-labelledby="contact-heading">
|
||||
<h2 id="contact-heading"><span class="sec-num" aria-hidden="true">6.</span> Contact</h2>
|
||||
<p>To initiate an engagement, send a <code>POST</code> to one of the following channels:</p>
|
||||
<div class="contact-grid">
|
||||
<a class="contact-card" href="https://cal.levkin.ca/ilia/consult" rel="noopener noreferrer">
|
||||
<span class="method post" aria-hidden="true">POST</span>
|
||||
<span class="contact-path">/consult</span>
|
||||
<span class="desc">15-minute discovery call<span class="sr-only"> (opens cal.levkin.ca)</span></span>
|
||||
</a>
|
||||
<a class="contact-card" href="mailto:ilia@levkine.ca?subject=Project%20enquiry">
|
||||
<span class="method post" aria-hidden="true">POST</span>
|
||||
<span class="contact-path">/email</span>
|
||||
<span class="desc">ilia@levkine.ca</span>
|
||||
</a>
|
||||
</div>
|
||||
<p class="copyright">© Levkin · Canadian software development</p>
|
||||
<a href="#main" class="back-top">↑ Back to top</a>
|
||||
</section>
|
||||
</article>
|
||||
<footer class="instrument-footer">
|
||||
<p>© Levkin Inc. · Canadian software development</p>
|
||||
<p>LK-SPEC-1.0 · levkin.ca · Effective 20 May 2026</p>
|
||||
</footer>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="./spec.js" type="module"></script>
|
||||
|
||||
820
spec/spec.css
820
spec/spec.css
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
<header class="layer-head"><button type="button" class="layer-id" data-goto="0">L0</button><span class="layer-name">foundation</span></header>
|
||||
<h1>Levkin</h1>
|
||||
<p class="tagline">Software · Canada · remote</p>
|
||||
<p class="body">Boutique engineering — production systems, automation, enterprise.</p>
|
||||
<p class="layer-copy">Boutique engineering — production systems, automation, enterprise.</p>
|
||||
<div class="chips"><span>15+ yrs</span><span>8h→2m</span><span>24/7</span></div>
|
||||
<p class="avail">Taking new engagements</p>
|
||||
</div></section></div>
|
||||
@ -31,37 +31,37 @@
|
||||
<div class="scroll-section" data-layer="1"><section class="layer layer-1"><div class="layer-tab" aria-hidden="true"></div><div class="layer-inner">
|
||||
<header class="layer-head"><button type="button" class="layer-id" data-goto="1">L1</button><span class="layer-name">application</span></header>
|
||||
<h2>Custom software</h2>
|
||||
<p class="body">Web apps, APIs, internal tools — TS · Python · .NET</p>
|
||||
<p class="layer-copy">Web apps, APIs, internal tools — TS · Python · .NET</p>
|
||||
</div></section></div>
|
||||
|
||||
<div class="scroll-section" data-layer="2"><section class="layer layer-2"><div class="layer-tab" aria-hidden="true"></div><div class="layer-inner">
|
||||
<header class="layer-head"><button type="button" class="layer-id" data-goto="2">L2</button><span class="layer-name">automation</span><a href="https://auto.levkin.ca" class="layer-link">auto.levkin.ca ↗</a></header>
|
||||
<h2>Automation</h2>
|
||||
<p class="body">n8n · Zapier · CI/CD · webhooks · LLMs</p>
|
||||
<p class="layer-copy">n8n · Zapier · CI/CD · webhooks · LLMs</p>
|
||||
</div></section></div>
|
||||
|
||||
<div class="scroll-section" data-layer="3"><section class="layer layer-3"><div class="layer-tab" aria-hidden="true"></div><div class="layer-inner">
|
||||
<header class="layer-head"><button type="button" class="layer-id" data-goto="3">L3</button><span class="layer-name">enterprise</span><a href="https://caseware.levkin.ca" class="layer-link">caseware.levkin.ca ↗</a></header>
|
||||
<h2>CaseWare & CaseView</h2>
|
||||
<p class="body">15+ years · CaseWare Intl, MNP, JazzIt</p>
|
||||
<p class="layer-copy">15+ years · CaseWare Intl, MNP, JazzIt</p>
|
||||
</div></section></div>
|
||||
|
||||
<div class="scroll-section" data-layer="4"><section class="layer layer-4"><div class="layer-tab" aria-hidden="true"></div><div class="layer-inner">
|
||||
<header class="layer-head"><button type="button" class="layer-id" data-goto="4">L4</button><span class="layer-name">quality</span><a href="https://iliadobkin.com" class="layer-link">iliadobkin.com ↗</a></header>
|
||||
<h2>Quality engineering</h2>
|
||||
<p class="body">Senior SDET · test automation · CI/CD · trace-driven QA</p>
|
||||
<p class="layer-copy">Senior SDET · test automation · CI/CD · trace-driven QA</p>
|
||||
</div></section></div>
|
||||
|
||||
<div class="scroll-section" data-layer="5"><section class="layer layer-5"><div class="layer-tab" aria-hidden="true"></div><div class="layer-inner">
|
||||
<header class="layer-head"><button type="button" class="layer-id" data-goto="5">L5</button><span class="layer-name">operations</span><a href="https://jobs.levkin.ca" class="layer-link">jobs.levkin.ca ↗</a></header>
|
||||
<h2>Job Ops</h2>
|
||||
<p class="body">Internal hiring orchestrator (auth required)</p>
|
||||
<p class="layer-copy">Internal hiring orchestrator (auth required)</p>
|
||||
</div></section></div>
|
||||
|
||||
<div class="scroll-section scroll-section--final" data-layer="6"><section class="layer layer-6"><div class="layer-tab" aria-hidden="true"></div><div class="layer-inner">
|
||||
<header class="layer-head"><button type="button" class="layer-id" data-goto="6">L6</button><span class="layer-name">interface</span></header>
|
||||
<h2>Engage</h2>
|
||||
<p class="body">Discover → Proposal → Ship → Maintain</p>
|
||||
<p class="layer-copy">Discover → Proposal → Ship → Maintain</p>
|
||||
<div class="contact-row">
|
||||
<a class="btn" href="https://cal.levkin.ca/ilia/consult">Book 15 min</a>
|
||||
<a class="btn btn-ghost" href="mailto:hello@levkine.ca">hello@levkine.ca</a>
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user