Rebuild stack-folder with sticky tab rail and site previews.

L0–L7 folders stack on scroll with aligned max depth, labeled tabs that
stay consistent when L7 joins the rail, Cal embeds, preview screenshots,
Playwright tests, and updated README.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ilia 2026-05-21 21:30:05 -04:00
parent 09b0f498ba
commit 21c75cdcba
28 changed files with 1669 additions and 348 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules/ node_modules/
dist/ dist/
.DS_Store .DS_Store
.scroll-debug/

View File

@ -2,25 +2,13 @@
Design concepts for the Levkin software development company homepage. Design concepts for the Levkin software development company homepage.
### Brand directions
| Option | Path | Vibe | | Option | Path | Vibe |
|--------|------|------| |--------|------|------|
| **Spec** | `/spec/` | RFC documentation, endpoints, iliadobkin.com | | **Spec** | `/spec/` | RFC documentation, endpoints, iliadobkin.com |
| **Slab** | `/slab/` | Brutalist poster | | **Cards** | `/stack/` | Dark overlapping sticky cards — click a layer to bring it forward |
| **Relay** | `/relay/` | Telegraph, decoded messages | | **Folder** | `/stack-folder/` | Manila folders, staggered tabs (L0L7), site previews |
| **Vault** | `/vault/` | Institutional trust |
### Stack variants (L0L6, scroll stops at interface) Open `/` to compare all three.
| Variant | Path | Metaphor |
|---------|------|----------|
| **Cards** | `/stack/` | Dark overlapping sticky cards |
| **Folder** | `/stack-folder/` | Manila folders, top tabs stay visible |
| **Trace** | `/stack-trace/` | Call-stack / devtools frames |
| **Rack** | `/stack-rack/` | Server rack 1U units with status LEDs |
Open `/` to compare all.
## Develop ## Develop
@ -30,6 +18,15 @@ npm install
npm run dev npm run dev
``` ```
Vite serves the multi-page app (default `http://localhost:5173`). If that port is busy, pass another: `npx vite --port 5175`.
| Page | URL |
|------|-----|
| Compare | `/` |
| Spec | `/spec/` |
| Cards | `/stack/` |
| Folders | `/stack-folder/` |
## Build ## Build
```bash ```bash
@ -37,6 +34,42 @@ npm run build
# output in dist/ # output in dist/
``` ```
## Folder site (`/stack-folder/`)
Eight manila folders (L0L7) with labeled tabs. Scroll stacks earlier tabs on a shared rail; L7 rises into the row last. Scroll depth is capped when all tabs align (`is-folded`).
- **L0** — Company + Cal embed
- **L1** — Scope + spec preview
- **L2** — Services
- **L3L5** — Automation, CaseWare, QA previews
- **L6** — Git repos preview
- **L7** — Terms + Cal embed
Modules: `shared/stack-scroll.js` (scroll/fold), `folder-rail.js` (side rail), `folder-cal.js` (Cal.com embeds).
### Preview screenshots
Each linked folder shows a screenshot; refresh captures with dev server running:
```bash
npm run dev # separate terminal
npm run capture-previews
```
Writes PNGs to `stack-folder/previews/`.
### Tests (Playwright)
Requires dev server on the URL you pass:
```bash
npm run dev
STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder
STACK_URL=http://localhost:5173/stack-folder/ npm run test:stack-scroll
```
`test:folder` checks tab alignment at max scroll and L7 jump. `test:stack-scroll` covers scroll/blur behavior on the card stack.
## Related sites ## Related sites
- [auto.levkin.ca](https://auto.levkin.ca) — automation - [auto.levkin.ca](https://auto.levkin.ca) — automation

View File

@ -24,7 +24,7 @@
min-height: 100vh; min-height: 100vh;
line-height: 1.5; line-height: 1.5;
} }
.wrap { max-width: 1100px; margin: 0 auto; padding: 4rem 1.5rem 6rem; } .wrap { max-width: 900px; margin: 0 auto; padding: 4rem 1.5rem 6rem; }
header { margin-bottom: 3rem; } header { margin-bottom: 3rem; }
.eyebrow { .eyebrow {
font-family: 'DM Mono', monospace; font-family: 'DM Mono', monospace;
@ -44,15 +44,7 @@
.grid { .grid {
display: grid; display: grid;
gap: 1.25rem; gap: 1.25rem;
} grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
@media (min-width: 640px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 900px) {
.grid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1200px) {
.grid { grid-template-columns: repeat(5, 1fr); }
} }
a.card { a.card {
display: block; display: block;
@ -68,12 +60,10 @@
transform: translateY(-3px); transform: translateY(-3px);
} }
.preview { .preview {
height: 130px; height: 110px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative;
overflow: hidden;
font-size: 0.7rem; font-size: 0.7rem;
} }
.preview--spec { .preview--spec {
@ -82,35 +72,6 @@
font-family: 'DM Mono', monospace; font-family: 'DM Mono', monospace;
letter-spacing: 0.06em; letter-spacing: 0.06em;
} }
.preview--slab {
background: #f5f5f0;
color: #0a0a0a;
font-weight: 800;
font-size: 2rem;
letter-spacing: -0.06em;
font-family: system-ui, sans-serif;
}
.preview--relay {
background: #1a1814;
color: #d4a574;
font-family: 'DM Mono', monospace;
letter-spacing: 0.2em;
}
.preview--relay::after {
content: '· · · ─ ─ ·';
position: absolute;
bottom: 1rem;
font-size: 0.55rem;
opacity: 0.5;
}
.preview--vault {
background: linear-gradient(160deg, #0c1410 0%, #1a2820 100%);
color: #c9b896;
font-family: 'DM Sans', sans-serif;
font-size: 0.65rem;
letter-spacing: 0.25em;
text-transform: uppercase;
}
.preview--stack { .preview--stack {
background: #0e0e10; background: #0e0e10;
flex-direction: column; flex-direction: column;
@ -119,17 +80,21 @@
} }
.preview--stack .card-layer { .preview--stack .card-layer {
width: 70%; width: 70%;
height: 14px; height: 12px;
border-radius: 3px; border-radius: 3px;
border: 1px solid rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.1);
background: linear-gradient(90deg, #2a2a32, #3a3a46); background: linear-gradient(90deg, #2a2a32, #3a3a46);
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
} }
.preview--stack .card-layer:nth-child(1) { width: 55%; opacity: 0.5; } .preview--stack .card-layer:nth-child(1) { width: 55%; opacity: 0.5; }
.preview--stack .card-layer:nth-child(2) { width: 62%; opacity: 0.65; } .preview--stack .card-layer:nth-child(2) { width: 62%; opacity: 0.7; }
.preview--stack .card-layer:nth-child(3) { width: 68%; opacity: 0.8; } .preview--stack .card-layer:nth-child(3) { width: 75%; background: #4a4a58; }
.preview--stack .card-layer:nth-child(4) { width: 75%; } .preview--folder {
.preview--stack .card-layer:nth-child(5) { width: 82%; background: #4a4a58; } background: #e8e2d4;
color: #2a2824;
font-family: 'DM Mono', monospace;
font-size: 0.65rem;
border-left: 12px solid #c9a86c;
}
.card-body { padding: 1.15rem 1.25rem 1.35rem; } .card-body { padding: 1.15rem 1.25rem 1.35rem; }
.card-body h2 { font-size: 1.05rem; font-weight: 600; margin-bottom: 0.3rem; } .card-body h2 { font-size: 1.05rem; font-weight: 600; margin-bottom: 0.3rem; }
.card-body p { font-size: 0.82rem; color: var(--muted); line-height: 1.4; } .card-body p { font-size: 0.82rem; color: var(--muted); line-height: 1.4; }
@ -141,13 +106,6 @@
color: var(--accent); color: var(--accent);
letter-spacing: 0.06em; letter-spacing: 0.06em;
} }
.kept {
display: inline-block;
font-family: 'DM Mono', monospace;
font-size: 0.6rem;
color: var(--muted);
margin-left: 0.35rem;
}
footer { footer {
margin-top: 3.5rem; margin-top: 3.5rem;
padding-top: 2rem; padding-top: 2rem;
@ -157,120 +115,43 @@
} }
footer a { color: var(--accent); text-decoration: none; } footer a { color: var(--accent); text-decoration: none; }
footer code { font-family: 'DM Mono', monospace; font-size: 0.75rem; } footer code { font-family: 'DM Mono', monospace; font-size: 0.75rem; }
.section-label {
font-family: 'DM Mono', monospace;
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
margin: 2rem 0 1rem;
}
.grid-stacks { margin-bottom: 0; }
.preview--folder {
background: #e8e2d4;
color: #2a2824;
font-family: 'DM Mono', monospace;
font-size: 0.65rem;
border-left: 12px solid #c9a86c;
}
.preview--rack {
background: #12141a;
color: #4ade80;
font-family: 'DM Mono', monospace;
font-size: 0.55rem;
letter-spacing: 0.15em;
border: 2px solid #2a3040;
}
.preview--trace {
background: #0d0d0f;
color: #6b9b6b;
font-family: 'DM Mono', monospace;
font-size: 0.6rem;
}
</style> </style>
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
<header> <header>
<p class="eyebrow">levkin.ca · round 3</p> <p class="eyebrow">levkin.ca</p>
<h1>Eight directions.</h1> <h1>Three directions.</h1>
<p class="lead">Five brand concepts + four stack variants (L0L6, stops on time). Spec updated with iliadobkin.com.</p> <p class="lead">Spec for the company story. Cards and Folder for the L0L6 scroll stack — click a layer to bring it to the front.</p>
</header> </header>
<p class="section-label">Brand</p>
<div class="grid"> <div class="grid">
<a class="card" href="/spec/"> <a class="card" href="/spec/">
<div class="preview preview--spec">GET /company → 200</div> <div class="preview preview--spec">GET /company → 200</div>
<div class="card-body"> <div class="card-body">
<h2>Spec <span class="kept">kept</span></h2> <h2>Spec</h2>
<p>Levkin as an RFC. Endpoints, schemas, required properties. Precise and documentation-native.</p> <p>Levkin as an RFC. Endpoints, schemas, required properties.</p>
<span class="tag">paper · RFC · precise</span> <span class="tag">paper · RFC</span>
</div> </div>
</a> </a>
<a class="card" href="/slab/">
<div class="preview preview--slab">LEV</div>
<div class="card-body">
<h2>Slab</h2>
<p>Brutalist poster. Massive type, hard edges, zero decoration. Confidence without explaining itself.</p>
<span class="tag">brutalist · bold · minimal</span>
</div>
</a>
<a class="card" href="/relay/">
<div class="preview preview--relay">RELAY</div>
<div class="card-body">
<h2>Relay</h2>
<p>Telegraph and signal chain. Messages arrive, get decoded. Communication as craft.</p>
<span class="tag">vintage · interactive · warm</span>
</div>
</a>
<a class="card" href="/vault/">
<div class="preview preview--vault">Secured</div>
<div class="card-body">
<h2>Vault</h2>
<p>Institutional trust. Deep green, brass accents. For enterprise clients who need gravitas.</p>
<span class="tag">trust · enterprise · calm</span>
</div>
</a>
</div>
<p class="section-label">Stack variants (scroll test)</p>
<div class="grid grid-stacks">
<a class="card" href="/stack/"> <a class="card" href="/stack/">
<div class="preview preview--stack"> <div class="preview preview--stack">
<span class="card-layer"></span><span class="card-layer"></span><span class="card-layer"></span><span class="card-layer"></span><span class="card-layer"></span> <span class="card-layer"></span><span class="card-layer"></span><span class="card-layer"></span>
</div> </div>
<div class="card-body"> <div class="card-body">
<h2>Stack · Cards</h2> <h2>Cards</h2>
<p>L0L6 overlapping sticky cards. Scroll stops at L6.</p> <p>Dark sticky stack. Scroll or click L0L6 to focus a layer.</p>
<span class="tag">default · dark · technical</span> <span class="tag">stack · scroll</span>
</div> </div>
</a> </a>
<a class="card" href="/stack-folder/"> <a class="card" href="/stack-folder/">
<div class="preview preview--folder">L0│tab</div> <div class="preview preview--folder">L0│ L1│ L2│</div>
<div class="card-body"> <div class="card-body">
<h2>Stack · Folder</h2> <h2>Folder</h2>
<p>Tabs on top, staggered left — all readable. Click tab or L0L6 rail to jump.</p> <p>Spec as a filing cabinet — manila tabs, clause sections, pull a file forward.</p>
<span class="tag">folder · tabs · office</span> <span class="tag">folder · spec</span>
</div>
</a>
<a class="card" href="/stack-rack/">
<div class="preview preview--rack">U0▮U1▮</div>
<div class="card-body">
<h2>Stack · Rack</h2>
<p>Server rack 1U units — LEDs, handles, infra stack.</p>
<span class="tag">rack · infra · LEDs</span>
</div>
</a>
<a class="card" href="/stack-trace/">
<div class="preview preview--trace">at Levkin.*</div>
<div class="card-body">
<h2>Stack · Trace</h2>
<p>Call-stack frames — iliadobkin.com as quality frame.</p>
<span class="tag">trace · devtools · mono</span>
</div> </div>
</a> </a>
</div> </div>

View File

@ -6,7 +6,10 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"capture-previews": "node scripts/capture-previews.mjs",
"test:folder": "node scripts/test-stack-folder.mjs",
"test:stack-scroll": "node scripts/test-stack-scroll.mjs"
}, },
"devDependencies": { "devDependencies": {
"playwright": "^1.60.0", "playwright": "^1.60.0",

View File

@ -0,0 +1,82 @@
#!/usr/bin/env node
/** Refresh stack-folder/previews/*.png — run dev server first for local shots */
import { chromium } from 'playwright';
import { mkdirSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const out = join(root, 'stack-folder/previews');
const base = process.env.PREVIEW_BASE || 'http://localhost:5177';
mkdirSync(out, { recursive: true });
const shots = [
['spec', `${base}/spec/`],
['stack', `${base}/stack/`],
['auto', 'https://auto.levkin.ca'],
['caseware', 'https://caseware.levkin.ca'],
['iliadobkin', 'https://iliadobkin.com'],
['git-repos', 'https://git.levkin.ca/explore/repos'],
['cal', 'https://cal.levkin.ca/ilia/consult'],
];
async function setCalTheme(page, mode) {
await page.evaluate((want) => {
const root = document.documentElement;
const btn = [...document.querySelectorAll('button, [role="button"], label, a')].find(
(el) => new RegExp(want, 'i').test(el.textContent || el.getAttribute('aria-label') || ''),
);
if (btn) btn.click();
if (want === 'dark') {
root.dataset.theme = 'dark';
root.classList.add('dark');
} else {
root.dataset.theme = 'light';
root.classList.remove('dark');
}
}, mode);
}
async function captureCal(page, outDir, theme) {
const url = 'https://cal.levkin.ca/ilia/consult';
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 25000 });
await page.waitForTimeout(1200);
await setCalTheme(page, theme);
await page.waitForTimeout(1500);
await page.evaluate(() => {
window.scrollTo(0, 120);
});
await page.waitForTimeout(600);
const file = theme === 'light' ? 'cal-light.png' : 'cal-dark.png';
await page.screenshot({
path: join(outDir, file),
clip: { x: 0, y: 0, width: 1280, height: 680 },
});
console.log(`${file}`);
}
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
for (const [name, url] of shots) {
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
await page.waitForTimeout(1500);
await page.screenshot({ path: join(out, `${name}.png`) });
console.log(`${name}.png`);
} catch (err) {
console.warn(`${name}: ${err.message}`);
}
}
try {
await captureCal(page, out, 'dark');
await captureCal(page, out, 'light');
} catch (err) {
console.warn(`✗ cal captures: ${err.message}`);
}
await browser.close();

View File

@ -0,0 +1,64 @@
/**
* Tab rail alignment tests for /stack-folder/.
* Run: STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder
*/
import { chromium } from 'playwright';
const URL = process.env.STACK_URL || 'http://localhost:5173/stack-folder/';
const VERBOSE = process.env.VERBOSE === '1';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
await page.goto(URL, { waitUntil: 'networkidle' });
async function tabTops() {
return page.evaluate(() => {
const tabs = [...document.querySelectorAll('.mount .tab')];
const stick =
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--stack-stick')) * 16;
return {
scrollY: window.scrollY,
isFolded: document.querySelector('.mount')?.classList.contains('is-folded'),
stick,
tabs: tabs.map((t) => ({
code: t.querySelector('.tab-code')?.textContent,
top: Math.round(t.getBoundingClientRect().top),
left: Math.round(t.getBoundingClientRect().left),
})),
};
});
}
if (VERBOSE) console.log('initial', await tabTops());
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
await page.waitForTimeout(600);
if (VERBOSE) console.log('max scroll', await tabTops());
await page.click('[data-goto="7"]');
await page.waitForTimeout(800);
if (VERBOSE) console.log('goto L7', await tabTops());
const fail = await page.evaluate(() => {
const tabs = [...document.querySelectorAll('.mount .tab')];
const tops = tabs.map((t) => t.getBoundingClientRect().top);
const min = Math.min(...tops);
const max = Math.max(...tops);
const l7 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L7');
const l0 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L0');
const issues = [];
if (max - min > 30) issues.push(`tab row spread ${Math.round(max - min)}px (want ≤30)`);
if (l7 && l0 && Math.abs(l7.getBoundingClientRect().top - l0.getBoundingClientRect().top) > 30)
issues.push('L7 not aligned with L0');
if (!document.querySelector('.mount')?.classList.contains('is-folded'))
issues.push('mount not folded at max scroll');
return issues;
});
await browser.close();
if (fail.length) {
console.error('FAIL:', fail.join('; '));
process.exit(1);
}
console.log('PASS: stack-folder tab rail');

View File

@ -0,0 +1,118 @@
/**
* Automated scroll/blur tests for stack-folder.
* Run: node scripts/test-stack-scroll.mjs
*/
import { chromium } from 'playwright';
const URL = process.env.STACK_URL || 'http://localhost:5173/stack-folder/';
const VIEWPORT = { width: 1280, height: 800 };
function fail(msg) {
console.error('FAIL:', msg);
process.exitCode = 1;
}
function pass(msg) {
console.log('PASS:', msg);
}
async function readState(page) {
return page.evaluate(() => {
const stick =
parseFloat(getComputedStyle(document.documentElement).fontSize) * 3;
const l7Tab = document.querySelector('.f7 .tab');
const l0 = document.querySelector('.f0');
const l0Body = document.querySelector('.f0 .body');
const folderBlur = l0?.style.getPropertyValue('--stack-blur') || '';
const filter = l0Body ? getComputedStyle(l0Body).filter : '';
const blurMatch = filter.match(/blur\(([\d.]+)px\)/);
const blurPx = blurMatch ? parseFloat(blurMatch[1]) : 0;
return {
scrollY: window.scrollY,
docMax: document.documentElement.scrollHeight - innerHeight,
l7TabTop: l7Tab?.getBoundingClientRect().top ?? null,
stick,
l0Covered: l0?.classList.contains('is-covered'),
l0BlurPx: blurPx,
folderBlur,
l0Filter: filter,
runway: getComputedStyle(document.querySelector('.mount')).getPropertyValue(
'--stack-runway',
),
};
});
}
async function main() {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: VIEWPORT });
await page.goto(URL, { waitUntil: 'networkidle' });
await page.waitForTimeout(400);
const top = await readState(page);
if (top.l0Covered || top.l0BlurPx > 0.1) {
fail(`L0 blurred at top (covered=${top.l0Covered}, blur=${top.l0BlurPx})`);
} else {
pass('L0 clear at scroll top');
}
await page.evaluate(() => window.scrollTo(0, 999999));
await page.waitForTimeout(350);
const end = await readState(page);
if (end.l7TabTop === null) fail('L7 tab missing');
else if (Math.abs(end.l7TabTop - end.stick) > 8) {
fail(`L7 tab not on stick: top=${end.l7TabTop} stick=${end.stick} scrollY=${end.scrollY}`);
} else {
pass(`L7 on stick at max scroll (y=${end.scrollY}, tabTop=${end.l7TabTop.toFixed(1)})`);
}
if (end.l0BlurPx < 2) {
fail(`L0 not blurred when stacked (blur=${end.l0BlurPx})`);
} else {
pass(`L0 blurred when stacked (blur=${end.l0BlurPx}px)`);
}
const midY = Math.floor(end.scrollY * 0.45);
await page.evaluate((y) => window.scrollTo(0, y), midY);
await page.waitForTimeout(200);
const mid = await readState(page);
if (mid.l0BlurPx > 2) {
fail(`L0 still heavily blurred mid-scroll out (blur=${mid.l0BlurPx} at y=${mid.scrollY})`);
} else {
pass(`L0 unfades when scrolling out (blur=${mid.l0BlurPx}px at y=${mid.scrollY})`);
}
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(350);
const back = await readState(page);
if (back.l0Covered || back.l0BlurPx > 0.1) {
fail(`L0 still blurred after scroll to top (covered=${back.l0Covered}, blur=${back.l0BlurPx})`);
} else {
pass('L0 unfades after scroll back to top');
}
const maxY = end.scrollY;
await page.evaluate((y) => window.scrollTo(0, y), maxY);
await page.waitForTimeout(200);
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(350);
const back2 = await readState(page);
if (back2.l0Covered || back2.l0BlurPx > 0.1) {
fail(`L0 stuck after down-up cycle (blur=${back2.l0BlurPx})`);
} else {
pass('L0 unfades after full down-up cycle');
}
await browser.close();
if (process.exitCode) {
console.log('\nTests failed.');
process.exit(1);
}
console.log('\nAll tests passed.');
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@ -1,6 +1,6 @@
/* Spacer after L6 so scroll can stop */
.stack-stop, .stack-stop,
.stop { .stop {
height: 1px; height: 0;
margin-bottom: 4rem; margin: 0;
pointer-events: none;
} }

View File

@ -1,43 +1,329 @@
/** Scroll depth + jump — panels are [data-layer] sticky cards */ /** Scroll depth, jump, folder fold state */
export function initStackScroll(options = {}) { export function initStackScroll(options = {}) {
const { const {
sectionSelector = '.layer[data-layer], .folder[data-layer], .frame[data-layer], .unit[data-layer]', sectionSelector = '.layer[data-layer], .folder[data-layer]',
depthEl = document.getElementById('depth'), depthEl = document.getElementById('depth'),
depthPrefix = 'L', depthPrefix = 'L',
tabSelector = '[data-goto], .jump', tabSelector = '[data-goto], .jump, .layer-id',
mountSelector = '.mount',
foldTabs = false,
interactionMode = 'pin',
} = options; } = options;
const sections = document.querySelectorAll(sectionSelector); const sections = document.querySelectorAll(sectionSelector);
if (!sections.length) return; if (!sections.length) return;
const mid = () => window.innerHeight * 0.45; const mount = document.querySelector(mountSelector);
const lastLayer = sections.length - 1;
const scrollPad = 56;
let frontLayer = null;
let layerScrollTops = [];
let maxScrollY = Infinity;
let measuringScroll = false;
const deckTabs = () =>
mount ? [...mount.querySelectorAll('.folder .tab')] : [];
function updateDepth() { function layerAnchor(el) {
let active = 0; return el.querySelector('.tab') || el.querySelector('.body') || el;
sections.forEach((el) => { }
const r = el.getBoundingClientRect();
if (r.top <= mid() && r.bottom > mid()) active = Number(el.dataset.layer);
});
if (depthEl) depthEl.textContent = `${depthPrefix}${active}`;
document.querySelectorAll('.stack-ruler button, .stack-ruler [data-goto], .tab-rail button, .tab[data-goto]').forEach((tab) => { function tabRowMetrics() {
const n = tab.dataset.layer ?? tab.dataset.goto; const tabs = deckTabs();
const stick = stackStickPx();
const tops = tabs.map((t) => t.getBoundingClientRect().top);
const spread = tops.length ? Math.max(...tops) - Math.min(...tops) : 0;
const onStick = tops.length
? tops.every((top) => Math.abs(top - stick) <= 6)
: false;
return { tabs, stick, spread, onStick };
}
function stackStickPx() {
const root = document.documentElement;
const raw = getComputedStyle(root).getPropertyValue('--stack-stick').trim();
const n = parseFloat(raw);
if (!Number.isFinite(n)) return scrollPad;
if (raw.endsWith('rem')) {
return n * parseFloat(getComputedStyle(root).fontSize);
}
if (raw.endsWith('px')) return n;
return n;
}
function captureLayerAnchors(force = false) {
if (!force && window.scrollY > 8 && layerScrollTops.length) return;
const stick = stackStickPx();
layerScrollTops = [...sections].map((el) =>
Math.max(0, layerAnchor(el).offsetTop - stick),
);
}
function lastTabEl() {
return sections[lastLayer]?.querySelector('.tab') ?? null;
}
function measureDeskEnd() {
const tab = lastTabEl();
if (!tab) return 0;
return Math.max(0, tab.offsetTop - stackStickPx());
}
function ensureStackRunway() {
if (!mount) return;
measuringScroll = true;
try {
captureLayerAnchors(true);
mount.style.setProperty('--stack-runway', '0px');
void mount.offsetHeight;
const stick = stackStickPx();
const tab = lastTabEl();
if (!tab) return;
for (let i = 0; i < 12; i++) {
const docMax = Math.max(
0,
document.documentElement.scrollHeight - window.innerHeight,
);
window.scrollTo({ top: docMax, behavior: 'auto' });
void mount.offsetHeight;
const shortfall = Math.ceil(tab.getBoundingClientRect().top - stick);
if (shortfall <= 1) break;
const cur =
parseFloat(getComputedStyle(mount).getPropertyValue('--stack-runway')) || 0;
mount.style.setProperty('--stack-runway', `${cur + shortfall + 2}px`);
void mount.offsetHeight;
captureLayerAnchors(true);
}
} finally {
measuringScroll = false;
window.scrollTo({ top: 0, behavior: 'auto' });
}
}
function tabsAlignedAtScroll() {
const { tabs, stick, spread } = tabRowMetrics();
if (!tabs.length) return false;
const tops = tabs.map((t) => t.getBoundingClientRect().top);
return (
spread <= 1 &&
tops.every((top) => Math.abs(top - stick) <= 2)
);
}
/** Furthest scroll Y where every tab is flush on the stick line */
function computeMaxScrollAligned() {
const docMax = Math.max(
0,
document.documentElement.scrollHeight - window.innerHeight,
);
if (!deckTabs().length) return 0;
measuringScroll = true;
try {
for (let y = docMax; y >= 0; y -= 1) {
window.scrollTo({ top: y, behavior: 'auto' });
void mount?.offsetHeight;
if (tabsAlignedAtScroll()) return y;
}
return 0;
} finally {
measuringScroll = false;
window.scrollTo({ top: 0, behavior: 'auto' });
}
}
function captureMaxScroll() {
if (window.scrollY <= 8) {
captureLayerAnchors(true);
ensureStackRunway();
captureLayerAnchors(true);
}
maxScrollY = computeMaxScrollAligned();
}
function isFolded() {
if (!foldTabs || !mount) return false;
if (deckTabs().length < sections.length) return false;
return window.scrollY > 120 && tabsAlignedAtScroll();
}
function updateFolded() {
if (!mount) return;
mount.classList.toggle('is-folded', isFolded());
}
function clampScroll() {
if (measuringScroll) return;
if (window.scrollY > maxScrollY) {
window.scrollTo({ top: maxScrollY, behavior: 'auto' });
}
}
function layerTabTitle(layer) {
const section = sections[layer];
const tab = section?.querySelector('.tab');
if (!tab) return `${depthPrefix}${layer}`;
const code = tab.querySelector('.tab-code')?.textContent?.trim() ?? `${depthPrefix}${layer}`;
const label = tab.querySelector('.tab-label')?.textContent?.trim() ?? '';
return `${code}${label}`;
}
function setIndicators(active) {
if (depthEl) {
depthEl.textContent =
active === lastLayer && isFolded()
? layerTabTitle(active)
: `${depthPrefix}${active}`;
}
document.querySelectorAll('.stack-ruler button, .stack-ruler [data-goto], .tab-rail button, .tab[data-goto], .layer-id').forEach((el) => {
const n = el.dataset.layer ?? el.dataset.goto;
if (n === undefined) return; if (n === undefined) return;
tab.classList.toggle('active', Number(n) === active); el.classList.toggle('active', Number(n) === active);
}); });
} }
function setFront(layer) {
frontLayer = layer;
sections.forEach((el) => {
el.classList.toggle('is-front', layer !== null && Number(el.dataset.layer) === layer);
});
if (layer !== null) setIndicators(layer);
}
function clearFront() {
setFront(null);
}
function scrollToLayer(layer) {
if (isFolded() && layerScrollTops[layer] !== undefined) {
window.scrollTo({ top: layerScrollTops[layer], behavior: 'smooth' });
return;
}
const target =
document.getElementById(`layer-${layer}`) ||
document.querySelector(`${sectionSelector}[data-layer="${layer}"]`);
if (!target) return;
let y;
if (layer === lastLayer) {
captureMaxScroll();
y = maxScrollY;
} else {
const anchor = layerAnchor(target);
y = Math.min(
anchor.getBoundingClientRect().top + window.scrollY - scrollPad,
maxScrollY,
);
}
const behavior = layer === lastLayer ? 'auto' : 'smooth';
window.scrollTo({ top: Math.max(0, y), behavior });
if (layer === lastLayer) {
requestAnimationFrame(updateDepth);
}
}
function navigateToLayer(layer) {
clearFront();
scrollToLayer(layer);
requestAnimationFrame(() => {
updateFolded();
updateDepth();
});
}
function openLayer(layer) {
if (interactionMode === 'navigate' || isFolded()) {
navigateToLayer(layer);
return;
}
if (frontLayer === layer) {
clearFront();
updateDepth();
scrollToLayer(layer);
return;
}
if (frontLayer !== null) {
clearFront();
scrollToLayer(layer);
requestAnimationFrame(updateDepth);
return;
}
setFront(layer);
scrollToLayer(layer);
}
function currentActiveLayer() {
if (isFolded()) return lastLayer;
const y = window.scrollY;
let active = 0;
for (let i = sections.length - 1; i >= 0; i--) {
const top = layerScrollTops[i];
if (top !== undefined && y >= top - 4) {
active = i;
break;
}
}
return active;
}
function updateCovered() {
sections.forEach((el) => {
el.classList.remove('is-covered');
el.style.removeProperty('--stack-blur');
});
}
function updateDepth() {
if (measuringScroll) return;
clampScroll();
updateFolded();
if (frontLayer !== null) return;
const active = currentActiveLayer();
updateCovered();
setIndicators(active);
window.dispatchEvent(
new CustomEvent('stack-depth', { detail: { active } }),
);
}
window.addEventListener('stack-goto-layer', (e) => {
const layer = Number(e.detail?.layer);
if (Number.isFinite(layer)) openLayer(layer);
});
sections.forEach((panel) => {
panel.addEventListener('click', (e) => {
if (e.target.closest('a[href], button[data-goto], .layer-id, .rail-tab, .tab[data-goto], .site-preview, .cta-block, .cal-slot, [data-cal-embed]')) return;
openLayer(Number(panel.dataset.layer));
});
});
document.querySelectorAll(tabSelector).forEach((tab) => { document.querySelectorAll(tabSelector).forEach((tab) => {
tab.addEventListener('click', (e) => { tab.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
const layer = tab.dataset.goto ?? tab.dataset.layer; e.stopPropagation();
const target = document.querySelector(`${sectionSelector}[data-layer="${layer}"]`); openLayer(Number(tab.dataset.goto ?? tab.dataset.layer));
if (!target) return;
const y = target.getBoundingClientRect().top + window.scrollY - 56;
window.scrollTo({ top: Math.max(0, y), behavior: 'smooth' });
}); });
}); });
window.addEventListener('scroll', updateDepth, { passive: true }); captureMaxScroll();
updateDepth(); updateDepth();
window.addEventListener('load', () => {
if (window.scrollY > 8) window.scrollTo(0, 0);
requestAnimationFrame(() => {
captureMaxScroll();
updateDepth();
});
});
window.addEventListener('resize', () => {
captureMaxScroll();
updateDepth();
}, { passive: true });
window.addEventListener('scroll', updateDepth, { passive: true });
} }

View File

@ -1,7 +1,10 @@
/* Original working sticky stack (cfca7aa) */ /* Sticky stack — one viewport of scroll per layer until L6 is on top */
:root { :root {
--stack-nav: 2.5rem; --stack-nav: 2.5rem;
--stack-stick: 3.5rem; --stack-stick: 3rem;
--stack-step: 2.75rem; --stack-tab-h: 1.72rem;
--stack-card-min: 52vh; --stack-step: 1.75rem;
--stack-gap: 0.35rem;
/* ~one folder body of scroll between layers (not a full viewport each) */
--stack-scroll-slot: 1.5rem;
} }

184
stack-folder/folder-cal.js Normal file
View File

@ -0,0 +1,184 @@
/**
* Cal.com inline embed paste from Event type consult Embed Inline.
* Uses embed.js (not a raw iframe). No allowed domains setting needed for this path.
* L0 = dark snippet, L7 = light snippet. Screenshot fallback if embed does not mount.
*/
const CAL_ORIGIN = 'https://cal.levkin.ca';
const CAL_LINK = 'ilia/consult';
const EMBED_SCRIPT = `${CAL_ORIGIN}/embed/embed.js`;
const SLOTS = {
dark: {
ns: 'consult-l0',
inlineConfig: {
layout: 'week_view',
useSlotsViewOnSmallScreen: 'true',
theme: 'dark',
},
ui: { theme: 'dark', hideEventTypeDetails: true, layout: 'week_view' },
},
light: {
ns: 'consult-l7',
inlineConfig: {
layout: 'week_view',
useSlotsViewOnSmallScreen: 'true',
theme: 'light',
},
ui: { theme: 'light', hideEventTypeDetails: true, layout: 'week_view' },
},
};
let calApiReady;
/** Loader from Cal embed UI */
function bootCalLoader() {
if (window.Cal?.loaded) return;
(function (C, A, L) {
const p = (a, ar) => {
a.q.push(ar);
};
const d = C.document;
C.Cal =
C.Cal ||
function () {
const cal = C.Cal;
const ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement('script')).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () {
p(api, arguments);
};
const namespace = ar[1];
api.q = api.q || [];
if (typeof namespace === 'string') {
cal.ns[namespace] = cal.ns[namespace] || api;
p(cal.ns[namespace], ar);
p(cal, ['initNamespace', namespace]);
} else p(cal, ar);
return;
}
p(cal, ar);
};
})(window, EMBED_SCRIPT, 'init');
}
function waitForEmbedScript() {
return new Promise((resolve) => {
const finish = () => resolve(window.Cal);
const attach = (s) => {
if (s.dataset.calReady) {
finish();
return;
}
s.addEventListener(
'load',
() => {
s.dataset.calReady = '1';
finish();
},
{ once: true },
);
s.addEventListener('error', () => resolve(null), { once: true });
};
const existing = document.querySelector(`script[src="${EMBED_SCRIPT}"]`);
if (existing) {
attach(existing);
return;
}
const mo = new MutationObserver(() => {
const s = document.querySelector(`script[src="${EMBED_SCRIPT}"]`);
if (!s) return;
mo.disconnect();
attach(s);
});
mo.observe(document.head, { childList: true });
window.setTimeout(() => {
mo.disconnect();
finish();
}, 12000);
});
}
function loadCalApi() {
if (!calApiReady) {
bootCalLoader();
/* First init pulls in embed.js (same as Cals generated snippet) */
window.Cal('init', 'consult', { origin: CAL_ORIGIN });
calApiReady = waitForEmbedScript();
}
return calApiReady;
}
function mountInline(slot) {
const targetId = slot.dataset.calTarget;
const themeKey = slot.dataset.calTheme === 'light' ? 'light' : 'dark';
const spec = SLOTS[themeKey];
const el = document.getElementById(targetId);
if (!el || !spec || !window.Cal?.ns) return;
window.Cal('init', spec.ns, { origin: CAL_ORIGIN });
window.Cal.ns[spec.ns]('inline', {
elementOrSelector: `#${targetId}`,
config: spec.inlineConfig,
calLink: CAL_LINK,
});
window.Cal.ns[spec.ns]('ui', spec.ui);
}
function watchSlot(slot) {
const targetId = slot.dataset.calTarget;
const el = document.getElementById(targetId);
if (!el) return;
const showFallback = () => {
slot.classList.add('is-blocked');
slot.classList.remove('is-embedded');
};
const showEmbed = () => {
slot.classList.remove('is-blocked');
slot.classList.add('is-embedded');
};
showFallback();
const hasLiveEmbed = () => {
const iframe = el.querySelector('iframe');
return Boolean(iframe && iframe.offsetHeight > 60);
};
const obs = new MutationObserver(() => {
if (hasLiveEmbed()) showEmbed();
});
obs.observe(el, { childList: true, subtree: true });
window.setTimeout(() => {
obs.disconnect();
if (!hasLiveEmbed()) showFallback();
}, 8000);
}
export function initCalEmbeds() {
const slots = [...document.querySelectorAll('[data-cal-embed]')];
if (!slots.length) return;
loadCalApi()
.then((Cal) => {
if (!Cal) return;
slots.forEach((slot) => {
mountInline(slot);
watchSlot(slot);
});
})
.catch(() => {
/* screenshots + ↗ */
});
}

View File

@ -0,0 +1,32 @@
/** Fixed rail — all L0L7 visible; click to jump (syncs with scroll) */
export function initRailRoller(rail = document.querySelector('.tab-rail')) {
const tabs = rail ? [...rail.querySelectorAll('.rail-tab')] : [];
if (!rail || !tabs.length) return;
function setActive(layer) {
const n = Math.max(0, Math.min(tabs.length - 1, layer));
tabs.forEach((btn, i) => btn.classList.toggle('active', i === n));
}
function gotoLayer(layer) {
setActive(layer);
window.dispatchEvent(
new CustomEvent('stack-goto-layer', { detail: { layer } }),
);
}
tabs.forEach((btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
gotoLayer(Number(btn.dataset.layer ?? btn.dataset.goto));
});
});
window.addEventListener('stack-depth', (e) => {
const layer = Number(e.detail?.active);
if (Number.isFinite(layer)) setActive(layer);
});
setActive(0);
}

View File

@ -2,142 +2,687 @@
@import '../shared/stack-layout.css'; @import '../shared/stack-layout.css';
:root { :root {
--tab-offset: 2.35rem; --tab-offset: 2.1rem;
--mono: 'IBM Plex Mono', monospace; --mono: 'IBM Plex Mono', monospace;
--sans: 'Instrument Sans', system-ui, sans-serif; --sans: 'Instrument Sans', system-ui, sans-serif;
--text: 'Instrument Sans', system-ui, sans-serif;
--display: 'Literata', Georgia, serif;
--hi: #fff59d;
--hi-edge: #f5e06a;
--desk: #3d3830;
--manila: #ebe4d4;
--manila-edge: #c9bea8;
--manila-dark: #d8cfbc;
--ink: #1c1a16;
--ink-muted: #4a4640;
--l0-tab: #c9a86c;
--l0-tab-fg: #2a2824;
--l1-tab: #a8c4d4;
--l1-tab-fg: #1a2830;
--l2-tab: #b8d4a8;
--l2-tab-fg: #1a2818;
--l3-tab: #d4b8c4;
--l3-tab-fg: #2a1820;
--l4-tab: #d4c8a8;
--l4-tab-fg: #2a2418;
--l5-tab: #c4c4c4;
--l5-tab-fg: #2a2a2a;
--l6-tab: #6a7a8a;
--l6-tab-fg: #f0ece4;
--l7-tab: #2a4a6b;
--l7-tab-fg: #ebe4d4;
} }
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--sans); background: #2a2824; color: #1a1814; } body {
font-family: var(--sans);
.nav { background: var(--desk);
position: fixed; top: 0; left: 0; right: 0; z-index: 400; color: var(--ink);
display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; padding: 0.5rem 1rem; background-image:
font-family: var(--mono); font-size: 0.62rem; linear-gradient(90deg, rgba(0,0,0,0.04) 1px, transparent 1px),
background: rgba(42,40,36,0.97); color: #c4b8a8; linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px);
background-size: 24px 24px;
} }
.nav a { color: #8a8278; text-decoration: none; } .site-header {
.nav a:hover { color: #d4a574; } position: fixed;
.depth { margin-left: auto; color: #d4a574; font-weight: 600; } top: 0;
left: 0;
right: 0;
z-index: 400;
padding: 0.5rem 1rem;
font-family: var(--mono);
font-size: 0.62rem;
letter-spacing: 0.02em;
text-align: center;
color: #b8ad9d;
background: rgba(42, 40, 36, 0.97);
border-bottom: 1px solid #59534a;
}
.site-header a {
color: #d4a574;
text-decoration: none;
}
.site-header a:hover {
color: #ebc39a;
}
.tab-rail { .tab-rail {
position: fixed; position: fixed;
top: 2.75rem; top: calc(var(--stack-nav) + 0.35rem);
right: max(0.5rem, calc(50% - 340px)); left: calc(50% + 20rem + 1.35rem);
right: auto;
z-index: 500; z-index: 500;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.2rem; gap: 0.15rem;
padding: 0.35rem; padding: 0.3rem;
background: rgba(42,40,36,0.92); background: rgba(42,40,36,0.94);
border: 1px solid #4a4844; border: 1px solid #4a4844;
border-radius: 6px; border-radius: 0;
} }
.rail-tab { .rail-tab {
font-family: var(--mono); font-family: var(--mono);
font-size: 0.55rem; font-size: 0.55rem;
padding: 0.2rem 0.45rem; padding: 0.22rem 0.5rem;
border: none; border: 1px solid transparent;
border-radius: 3px; border-radius: 0;
background: transparent; background: transparent;
color: #8a8278; color: #8a8278;
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
min-width: 2.1rem;
} }
.rail-tab:hover, .rail-tab[data-layer="0"]:hover,
.rail-tab.active { background: #c9a86c; color: #2a2824; } .rail-tab[data-layer="0"].active { background: var(--l0-tab); color: var(--l0-tab-fg); border-color: rgba(0,0,0,0.12); }
.rail-tab[data-layer="1"]:hover,
.rail-tab[data-layer="1"].active { background: var(--l1-tab); color: var(--l1-tab-fg); border-color: rgba(0,0,0,0.12); }
.rail-tab[data-layer="2"]:hover,
.rail-tab[data-layer="2"].active { background: var(--l2-tab); color: var(--l2-tab-fg); border-color: rgba(0,0,0,0.12); }
.rail-tab[data-layer="3"]:hover,
.rail-tab[data-layer="3"].active { background: var(--l3-tab); color: var(--l3-tab-fg); border-color: rgba(0,0,0,0.12); }
.rail-tab[data-layer="4"]:hover,
.rail-tab[data-layer="4"].active { background: var(--l4-tab); color: var(--l4-tab-fg); border-color: rgba(0,0,0,0.12); }
.rail-tab[data-layer="5"]:hover,
.rail-tab[data-layer="5"].active { background: var(--l5-tab); color: var(--l5-tab-fg); border-color: rgba(0,0,0,0.12); }
.rail-tab[data-layer="6"]:hover,
.rail-tab[data-layer="6"].active { background: var(--l6-tab); color: var(--l6-tab-fg); border-color: rgba(0,0,0,0.12); }
.rail-tab[data-layer="7"]:hover,
.rail-tab[data-layer="7"].active { background: var(--l7-tab); color: var(--l7-tab-fg); border-color: rgba(0,0,0,0.08); }
.mount { .mount {
--folder-body-h: 30rem;
width: min(640px, 100%); width: min(640px, 100%);
margin: 0 auto; margin: 0 auto;
padding: var(--stack-nav) 1rem 0; padding: calc(var(--stack-nav) + 0.75rem) 1rem 2rem;
}
/* Scroll step between layers (folder uses display:contents) */
.folder:not(.folder--last) .body {
margin-bottom: calc(var(--stack-scroll-slot) + 5.5rem);
}
.folder--last .body {
margin-bottom: var(--stack-runway, 0px);
} }
/* Same pattern as /stack/ — one sticky card, next covers previous */
.folder { .folder {
display: contents;
}
.folder .body {
position: sticky; position: sticky;
min-height: var(--stack-card-min); top: calc(var(--stack-stick) + var(--stack-tab-h) - 1px);
margin-bottom: 1.25rem; cursor: pointer;
width: 100%;
} }
.tab {
opacity: 1;
}
/* Each tab sticks above its folder body as you scroll the stack */
.tab { .tab {
position: sticky; position: sticky;
display: block; display: block;
width: fit-content; top: var(--stack-stick);
max-width: calc(100% - 1rem); height: var(--stack-tab-h);
box-sizing: border-box;
width: max-content;
max-width: none;
white-space: nowrap;
font-family: var(--mono); font-family: var(--mono);
font-size: 0.62rem; font-size: 0.58rem;
font-weight: 600; font-weight: 600;
letter-spacing: 0.04em; letter-spacing: 0.04em;
padding: 0.4rem 1rem 0.35rem; text-transform: uppercase;
border: 1px solid rgba(0,0,0,0.12); line-height: 1.2;
padding: 0.28rem 0.5rem 0;
border: 1px solid rgba(0,0,0,0.18);
border-bottom: none; border-bottom: none;
border-radius: 8px 8px 0 0; border-radius: 0;
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
z-index: 80; box-shadow: inset 0 1px 0 rgba(255,255,255,0.35);
box-shadow: 0 -2px 8px rgba(0,0,0,0.12);
} }
.tab:hover { filter: brightness(1.06); } .tab-label { font-weight: 500; opacity: 0.92; }
.tab:hover { filter: brightness(1.04); }
/* Every folder same height — previews fill remaining space */
.body { .body {
background: #e8e2d4; min-height: var(--folder-body-h);
border: 1px solid #c4b8a8; height: var(--folder-body-h);
border-top: none; display: flex;
border-radius: 0 10px 10px 10px; flex-direction: column;
padding: 1.25rem 1.4rem 1.5rem; overflow: hidden;
box-shadow: 0 10px 32px rgba(0,0,0,0.25); font-family: var(--sans);
font-size: 0.88rem;
font-weight: 400;
line-height: 1.55;
background: linear-gradient(180deg, var(--manila) 0%, #e3dbc8 100%);
border: 1px solid var(--manila-edge);
border-top: 2px solid var(--manila-dark);
border-radius: 0;
padding: 0.85rem 1rem 0.95rem 1.15rem;
box-shadow:
inset 5px 0 0 var(--manila-dark),
inset 0 -1px 0 rgba(0,0,0,0.06);
} }
.f0 { top: var(--stack-stick); z-index: 1; } .body-copy {
.f0 .tab { top: var(--stack-stick); margin-left: calc(var(--tab-offset) * 0); background: #c9a86c; color: #2a2824; } flex-shrink: 0;
overflow: hidden;
}
.f1 { top: calc(var(--stack-stick) + var(--stack-step)); z-index: 2; } .body-copy h2 {
.f1 .tab { top: calc(var(--stack-stick) + var(--stack-step)); margin-left: calc(var(--tab-offset) * 1); background: #a8c4d4; color: #1a2830; } margin-bottom: 0.25rem;
}
.f2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); z-index: 3; } .body-copy p:last-child {
.f2 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 2); margin-left: calc(var(--tab-offset) * 2); background: #b8d4a8; color: #1a2818; } margin-bottom: 0.35rem;
}
.f3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); z-index: 4; } .body-fill {
.f3 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 3); margin-left: calc(var(--tab-offset) * 3); background: #d4b8c4; color: #2a1820; } flex: 1 1 auto;
min-height: 0;
pointer-events: none;
}
.f4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); z-index: 5; } /* Preview folders: text on top, screenshot fills the rest */
.f4 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 4); margin-left: calc(var(--tab-offset) * 4); background: #d4c8a8; color: #2a2418; } .body--has-preview {
padding-bottom: 0.7rem;
overflow: hidden;
}
.f5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); z-index: 6; } .body--has-preview .body-copy {
.f5 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 5); margin-left: calc(var(--tab-offset) * 5); background: #c4c4c4; color: #2a2a2a; } max-height: 38%;
overflow: hidden;
overflow-y: hidden;
}
.f6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); z-index: 7; margin-bottom: 4rem; } .body--has-preview .body-copy p {
.f6 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 6); margin-left: calc(var(--tab-offset) * 6); background: #2a4a6b; color: #e8e2d4; } margin-bottom: 0.3rem;
font-size: 0.86rem;
line-height: 1.5;
}
.body--has-preview .body-copy h1 {
font-size: 1.2rem;
margin-bottom: 0.2rem;
}
.body--has-preview .body-copy h2 {
font-size: 1.05rem;
margin-bottom: 0.2rem;
}
.body--has-preview .body-copy .cta-block {
margin-top: 0.35rem;
}
.body--has-preview .terms-list {
margin-bottom: 0.25rem;
font-size: 0.95rem;
}
.body--has-preview .terms-list li {
margin-bottom: 0.2rem;
}
/* Screenshot preview — click opens site in new tab */
a.site-preview {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
margin: 0.4rem 0 0;
border: 1px solid var(--manila-edge);
background: #f6f2e8;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.5);
text-decoration: none;
color: inherit;
cursor: pointer;
overflow: hidden;
}
a.site-preview:hover {
border-color: #a89878;
filter: brightness(1.02);
}
.site-preview-bar {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.3rem 0.45rem;
background: #ddd6c8;
border-bottom: 1px solid var(--manila-edge);
font-family: var(--mono);
font-size: 0.52rem;
color: var(--ink-muted);
}
.site-preview-url {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.site-preview-go {
color: #2a4a6b;
font-weight: 600;
}
.site-preview-frame {
display: flex;
flex: 1 1 0;
min-height: 11rem;
overflow: hidden;
background: #1a1814;
}
.site-preview-img {
display: block;
width: 100%;
height: 100%;
min-height: 100%;
object-fit: cover;
object-position: top center;
background: #1a1814;
}
.cta-block {
position: relative;
z-index: 2;
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
margin: 0.55rem 0 0.35rem;
}
.body .btn {
font-family: var(--mono);
font-size: 0.68rem;
font-weight: 600;
line-height: 1.2;
letter-spacing: 0.01em;
}
.body .btn--primary {
background: #2a4a6b;
color: #fff !important;
border: 1px solid #1e3854;
}
.body .btn--primary:hover {
background: #1e3854;
color: #fff !important;
}
.body .btn--ghost {
background: #f6f2e8;
color: #1c3a5c !important;
border: 1px solid #8a9ab0;
}
.body .btn--ghost:hover {
background: #fff;
color: #2a4a6b !important;
}
.service-deep-list {
margin: 0 0 0.5rem 0;
padding: 0;
list-style: none;
font-size: 1.05rem;
line-height: 1.4;
color: var(--ink);
}
.service-deep-list > li {
margin-bottom: 0.55rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.service-deep-list > li:last-child {
border-bottom: none;
margin-bottom: 0.35rem;
}
.service-deep-list strong {
display: inline;
font-family: var(--sans);
font-size: inherit;
font-weight: 600;
color: var(--ink);
}
.hi {
font-weight: 600;
color: var(--ink);
}
.service-deep-list .xref {
display: block;
margin-top: 0.2rem;
}
.section-lead {
font-size: 0.88rem;
color: var(--ink-muted);
margin-bottom: 0.55rem;
}
/* L2 — dense copy, no internal scroll */
.folder--services .body-copy {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
overflow-y: hidden;
}
.folder--services .section-lead {
font-size: 0.86rem;
margin-bottom: 0.4rem;
line-height: 1.4;
}
.folder--services .service-deep-list {
font-size: 0.8rem;
line-height: 1.35;
}
.folder--services .service-deep-list > li {
margin-bottom: 0.35rem;
padding-bottom: 0.3rem;
}
.folder--services .service-deep-list strong {
font-size: 0.88rem;
display: inline;
margin-right: 0.2rem;
}
.folder--services .service-deep-list .xref {
display: inline;
margin-top: 0;
margin-left: 0.15rem;
}
.folder--services .body-copy > p:last-child {
font-size: 0.78rem;
margin-bottom: 0;
}
.body::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 28%;
height: 100%;
pointer-events: none;
background: linear-gradient(90deg, transparent, rgba(0,0,0,0.03));
}
/* Bodies pile by z-index; each tab sticks above its folder */
.f0 .body { z-index: 1; }
.f1 .body { z-index: 2; }
.f2 .body { z-index: 3; }
.f3 .body { z-index: 4; }
.f4 .body { z-index: 5; }
.f5 .body { z-index: 6; }
.f6 .body { z-index: 7; }
.f7 .body { z-index: 8; }
.f0 .tab { margin-left: calc(var(--tab-offset) * 0); z-index: 110; background: var(--l0-tab); color: var(--l0-tab-fg); }
.f1 .tab { margin-left: calc(var(--tab-offset) * 1); z-index: 111; background: var(--l1-tab); color: var(--l1-tab-fg); }
.f2 .tab { margin-left: calc(var(--tab-offset) * 2); z-index: 112; background: var(--l2-tab); color: var(--l2-tab-fg); }
.f3 .tab { margin-left: calc(var(--tab-offset) * 3); z-index: 113; background: var(--l3-tab); color: var(--l3-tab-fg); }
.f4 .tab { margin-left: calc(var(--tab-offset) * 4); z-index: 114; background: var(--l4-tab); color: var(--l4-tab-fg); }
.f5 .tab { margin-left: calc(var(--tab-offset) * 5); z-index: 115; background: var(--l5-tab); color: var(--l5-tab-fg); }
.f6 .tab { margin-left: calc(var(--tab-offset) * 6); z-index: 116; background: var(--l6-tab); color: var(--l6-tab-fg); }
.f7 .tab { margin-left: calc(var(--tab-offset) * 7); z-index: 117; background: var(--l7-tab); color: var(--l7-tab-fg); }
.file-id {
font-family: var(--mono);
font-size: 0.58rem;
letter-spacing: 0.06em;
color: var(--ink-muted);
margin-bottom: 0.35rem;
}
.body h1 {
font-family: var(--display);
font-size: 1.35rem;
font-weight: 600;
line-height: 1.25;
margin-bottom: 0.35rem;
color: var(--ink);
}
.body h2 {
font-family: var(--display);
font-size: 1.05rem;
font-weight: 600;
margin-bottom: 0.4rem;
color: var(--ink);
}
.lead, .body p, .clause {
font-size: 0.88rem;
color: var(--ink-muted);
line-height: 1.55;
margin-bottom: 0.45rem;
}
.stack-note, .xref {
font-family: var(--mono);
font-size: 0.68rem;
color: var(--ink-muted);
}
.body a:not(.btn) { color: #2a4a6b; font-weight: 600; }
.meta-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
margin: 0.5rem 0 0.65rem;
}
.meta-table th {
font-family: var(--mono);
font-size: 0.62rem;
font-weight: 500;
text-align: left;
color: var(--ink-muted);
padding: 0.2rem 0.65rem 0.2rem 0;
vertical-align: top;
width: 5.5rem;
}
.meta-table td { padding: 0.2rem 0; color: var(--ink); }
.badge {
display: inline-block;
font-family: var(--mono);
font-size: 0.58rem;
font-weight: 600;
letter-spacing: 0.08em;
padding: 0.12rem 0.4rem;
border: 1px solid var(--ink);
color: var(--ink);
}
.badge--avail { border-color: #3d6b3d; color: #2d5a2d; }
.avail { margin-top: 0.35rem; font-size: 0.82rem; }
.terms-list {
margin: 0 0 0.65rem 1rem;
font-size: 1.05rem;
color: var(--ink);
line-height: 1.45;
}
.terms-list strong {
font-weight: 600;
color: var(--ink);
}
.terms-list li { margin-bottom: 0.35rem; }
.meta-table--contact { margin-top: 0.5rem; }
.body h1 { font-size: 1.65rem; margin-bottom: 0.35rem; }
.body h2 { font-size: 1.25rem; margin-bottom: 0.4rem; }
.body p { font-size: 0.92rem; color: #4a4844; line-height: 1.5; }
.body a { color: #2a4a6b; }
.avail { font-family: var(--mono); font-size: 0.68rem; color: #3d6b3d; margin-top: 0.5rem; }
.btn { .btn {
font-family: var(--mono); font-size: 0.7rem; padding: 0.45rem 0.75rem; padding: 0.5rem 0.85rem;
background: #2a4a6b; color: #fff; text-decoration: none; border-radius: 4px; text-decoration: none;
margin-right: 0.4rem; display: inline-block; margin-top: 0.35rem; border-radius: 0;
margin-right: 0.35rem;
display: inline-block;
margin-top: 0.3rem;
} }
.btn.ghost { background: transparent; color: #2a4a6b; border: 1px solid #2a4a6b; }
.foot { /* Cal.com embed (needs embed allowlist on cal.levkin.ca for levkin.ca) */
display: flex; justify-content: space-between; .cal-slot {
width: min(640px, 100%); margin: 0 auto; display: flex;
padding: 0 1rem 2rem; font-family: var(--mono); font-size: 0.62rem; color: #6a6458; flex-direction: column;
flex: 1 1 auto;
min-height: 0;
margin: 0.4rem 0 0;
border: 1px solid var(--manila-edge);
background: #1a1814;
overflow: hidden;
}
.cal-slot .site-preview-bar {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.3rem 0.45rem;
background: #ddd6c8;
border-bottom: 1px solid var(--manila-edge);
font-family: var(--mono);
font-size: 0.52rem;
color: var(--ink-muted);
}
.cal-slot .site-preview-go {
color: #2a4a6b;
font-weight: 600;
text-decoration: none;
}
.cal-embed-frame {
position: relative;
flex: 1 1 auto;
min-height: 11rem;
background: #0a0a0a;
}
.cal-slot[data-cal-theme='light'] .cal-embed-frame {
background: #f5f3ee;
}
.cal-inline {
width: 100%;
height: 100%;
min-height: 14rem;
overflow: auto;
}
.cal-slot.is-blocked .cal-inline {
display: none;
}
.cal-slot.is-embedded .cal-inline iframe {
display: block;
width: 100%;
min-height: 14rem;
border: 0;
}
.cal-embed-fallback {
display: none;
position: absolute;
inset: 0;
text-decoration: none;
color: inherit;
}
.cal-slot.is-blocked .cal-embed-fallback {
display: flex;
flex-direction: column;
}
.cal-embed-fallback img {
flex: 1;
width: 100%;
object-fit: cover;
object-position: top center;
/* Crop Cal.com branding bar at bottom of screenshot */
max-height: calc(100% + 2rem);
margin-bottom: -2rem;
}
.cal-embed-fallback-label {
font-family: var(--mono);
font-size: 0.55rem;
text-align: center;
padding: 0.35rem;
background: rgba(0, 0, 0, 0.75);
color: #c4b8a8;
}
.service-note {
font-size: 0.78rem;
line-height: 1.4;
color: var(--ink-muted);
margin-bottom: 0.28rem;
}
.folder--services .service-deep-list > li {
margin-bottom: 0.28rem;
padding-bottom: 0.22rem;
}
.folder--services .body-copy > p:last-child {
margin-bottom: 0;
} }
.foot a { color: #6a6458; text-decoration: none; }
@media (max-width: 720px) { @media (max-width: 720px) {
.tab-rail { display: none; } .tab-rail { display: none; }
:root { --stack-step: 1.75rem; --tab-offset: 1.4rem; --stack-card-min: 48vh; } .mount { padding-right: 1rem; }
.f5 .tab { margin-left: calc(var(--tab-offset) * 4); } :root { --tab-offset: 1.75rem; }
.f6 .tab { margin-left: calc(var(--tab-offset) * 3); }
} }

View File

@ -3,21 +3,21 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Levkin — Stack Folder</title> <title>Levkin — Company Files</title>
<meta name="description" content="Levkin Inc. — company specification, filed by section." />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./folder.css" /> <link rel="stylesheet" href="./folder.css" />
</head> </head>
<body> <body>
<nav class="nav"> <header class="site-header desk-footer">
<a href="/">← options</a> Levkin Inc. · production software, automation, and handoff-ready systems ·
<a href="/stack/">cards</a> <a href="https://cal.levkin.ca/ilia/consult" target="_blank" rel="noopener noreferrer">book discovery call</a>
<a href="/stack-trace/">trace</a> </header>
<a href="/stack-rack/">rack</a>
<span class="depth" id="depth">L0</span>
</nav>
<div class="tab-rail" aria-label="Jump to layer"> <div class="tab-rail" id="tab-rail" aria-label="Layers L0L7">
<button type="button" class="rail-tab active" data-goto="0" data-layer="0">L0</button> <button type="button" class="rail-tab active" data-goto="0" data-layer="0">L0</button>
<button type="button" class="rail-tab" data-goto="1" data-layer="1">L1</button> <button type="button" class="rail-tab" data-goto="1" data-layer="1">L1</button>
<button type="button" class="rail-tab" data-goto="2" data-layer="2">L2</button> <button type="button" class="rail-tab" data-goto="2" data-layer="2">L2</button>
@ -25,74 +25,161 @@
<button type="button" class="rail-tab" data-goto="4" data-layer="4">L4</button> <button type="button" class="rail-tab" data-goto="4" data-layer="4">L4</button>
<button type="button" class="rail-tab" data-goto="5" data-layer="5">L5</button> <button type="button" class="rail-tab" data-goto="5" data-layer="5">L5</button>
<button type="button" class="rail-tab" data-goto="6" data-layer="6">L6</button> <button type="button" class="rail-tab" data-goto="6" data-layer="6">L6</button>
<button type="button" class="rail-tab" data-goto="7" data-layer="7">L7</button>
</div> </div>
<main class="mount"> <main class="mount">
<article class="folder f0" data-layer="0" id="layer-0"> <article class="folder f0" data-layer="0" id="layer-0">
<button type="button" class="tab" data-goto="0">L0 · foundation</button> <button type="button" class="tab" data-goto="0"><span class="tab-code">L0</span><span class="tab-label"> · Company</span></button>
<div class="body"> <div class="body body--has-preview">
<h1>Levkin</h1> <div class="body-copy">
<p>Software development · Canada · remote</p> <p class="file-id">LK-SPEC-1.0 · Cover sheet</p>
<p class="avail">Taking new engagements · 15+ yrs · 8h→2m</p> <h1>Levkin Software Development Company</h1>
<p class="lead">Custom applications, automation, and practice systems — <span class="hi">scoped</span>, <span class="hi">documented</span>, and built to run in <span class="hi">production</span>.</p>
<p class="avail"><span class="badge badge--avail">AVAILABLE</span> <span class="hi">Taking on new engagements</span> · remote NA &amp; EU</p>
<div class="cta-block">
<a class="btn btn--primary" href="https://cal.levkin.ca/ilia/consult" target="_blank" rel="noopener noreferrer">Book 15-minute discovery call</a>
<a class="btn btn--ghost" href="mailto:ilia@levkine.ca?subject=Project%20enquiry">ilia@levkine.ca</a>
</div>
</div>
<div class="cal-slot is-blocked" data-cal-embed data-cal-theme="dark" data-cal-target="cal-inline-l0">
<div class="site-preview-bar"><span class="site-preview-url">cal.levkin.ca · book a call</span><a class="site-preview-go" href="https://cal.levkin.ca/ilia/consult" target="_blank" rel="noopener noreferrer" aria-label="Open calendar in new tab"></a></div>
<div class="cal-embed-frame">
<div class="cal-inline" id="cal-inline-l0"></div>
<a class="cal-embed-fallback" href="https://cal.levkin.ca/ilia/consult" target="_blank" rel="noopener noreferrer">
<img src="./previews/cal-dark.png" alt="" decoding="async" />
</a>
</div>
</div>
</div> </div>
</article> </article>
<article class="folder f1" data-layer="1" id="layer-1"> <article class="folder f1" data-layer="1" id="layer-1">
<button type="button" class="tab" data-goto="1">L1 · application</button> <button type="button" class="tab" data-goto="1"><span class="tab-code">L1</span><span class="tab-label"> · Scope</span></button>
<div class="body"> <div class="body body--has-preview">
<h2>Custom software</h2> <div class="body-copy">
<p>Web apps, APIs, tools — TypeScript · Python · .NET · PostgreSQL</p> <h2>Scope</h2>
<p><span class="hi">Boutique practice</span> — not a staffing agency. Clear deliverable and handoff so your team can own what ships.</p>
</div>
<a class="site-preview" href="/spec/" target="_blank" rel="noopener noreferrer">
<span class="site-preview-bar"><span class="site-preview-url">levkin.ca / spec</span><span class="site-preview-go" aria-hidden="true"></span></span>
<span class="site-preview-frame"><img class="site-preview-img" src="./previews/spec.png" alt="" loading="lazy" decoding="async" /></span>
</a>
</div> </div>
</article> </article>
<article class="folder f2" data-layer="2" id="layer-2"> <article class="folder f2 folder--services" data-layer="2" id="layer-2">
<button type="button" class="tab" data-goto="2">L2 · automation</button> <button type="button" class="tab" data-goto="2"><span class="tab-code">L2</span><span class="tab-label"> · Services</span></button>
<div class="body"> <div class="body">
<h2>Automation</h2> <div class="body-copy">
<p>n8n · Zapier · CI/CD · LLMs · <a href="https://auto.levkin.ca">auto.levkin.ca</a></p> <h2>Services</h2>
<p class="section-lead">Primary lines — each engagement gets a <span class="hi">statement of work</span> after discovery.</p>
<ul class="service-deep-list">
<li><strong>Custom software</strong> — Web apps, APIs, and internal tools; fit the problem, not a preset stack.</li>
<li><strong>Automation</strong> — Reporting, notifications, data sync, ops in the background. <span class="xref"><a href="https://auto.levkin.ca" target="_blank" rel="noopener noreferrer">auto.levkin.ca</a></span></li>
<li><strong>CaseWare</strong> — Templates, releases, practice customization, pipeline work. <span class="xref"><a href="https://caseware.levkin.ca" target="_blank" rel="noopener noreferrer">caseware.levkin.ca</a></span></li>
<li><strong>Quality &amp; testing</strong> — Test strategy, automation, release confidence. <span class="xref"><a href="https://iliadobkin.com" target="_blank" rel="noopener noreferrer">iliadobkin.com</a></span></li>
</ul>
<p class="service-note"><strong>How we work:</strong> fixed scope · production-ready · handoff-friendly · right-sized solutions.</p>
<p class="service-note">Discovery call → written proposal → delivery with docs → optional support.</p>
</div>
<div class="body-fill" aria-hidden="true"></div>
</div> </div>
</article> </article>
<article class="folder f3" data-layer="3" id="layer-3"> <article class="folder f3" data-layer="3" id="layer-3">
<button type="button" class="tab" data-goto="3">L3 · enterprise</button> <button type="button" class="tab" data-goto="3"><span class="tab-code">L3</span><span class="tab-label"> · Automation</span></button>
<div class="body"> <div class="body body--has-preview">
<h2>CaseWare &amp; CaseView</h2> <div class="body-copy">
<p>15+ years · CaseWare Intl, MNP, JazzIt · <a href="https://caseware.levkin.ca">caseware.levkin.ca</a></p> <h2>Automation</h2>
<p>Workflows that run <span class="hi">reliably in the background</span> — reporting, sync, ops off your plate.</p>
</div>
<a class="site-preview" href="https://auto.levkin.ca" target="_blank" rel="noopener noreferrer">
<span class="site-preview-bar"><span class="site-preview-url">auto.levkin.ca</span><span class="site-preview-go" aria-hidden="true"></span></span>
<span class="site-preview-frame"><img class="site-preview-img" src="./previews/auto.png" alt="" loading="lazy" decoding="async" /></span>
</a>
</div> </div>
</article> </article>
<article class="folder f4" data-layer="4" id="layer-4"> <article class="folder f4" data-layer="4" id="layer-4">
<button type="button" class="tab" data-goto="4">L4 · quality</button> <button type="button" class="tab" data-goto="4"><span class="tab-code">L4</span><span class="tab-label"> · Enterprise</span></button>
<div class="body"> <div class="body body--has-preview">
<h2>Quality engineering</h2> <div class="body-copy">
<p>Senior SDET · <a href="https://iliadobkin.com">iliadobkin.com</a></p> <h2>CaseWare</h2>
<p>Templates, releases, and practice customization — from small fixes to full pipeline overhauls.</p>
</div>
<a class="site-preview" href="https://caseware.levkin.ca" target="_blank" rel="noopener noreferrer">
<span class="site-preview-bar"><span class="site-preview-url">caseware.levkin.ca</span><span class="site-preview-go" aria-hidden="true"></span></span>
<span class="site-preview-frame"><img class="site-preview-img" src="./previews/caseware.png" alt="" loading="lazy" decoding="async" /></span>
</a>
</div> </div>
</article> </article>
<article class="folder f5" data-layer="5" id="layer-5"> <article class="folder f5" data-layer="5" id="layer-5">
<button type="button" class="tab" data-goto="5">L5 · operations</button> <button type="button" class="tab" data-goto="5"><span class="tab-code">L5</span><span class="tab-label"> · Portfolio</span></button>
<div class="body"> <div class="body body--has-preview">
<h2>Job Ops</h2> <div class="body-copy">
<p>Internal hiring orchestrator · <a href="https://jobs.levkin.ca">jobs.levkin.ca</a></p> <h2>Quality &amp; testing</h2>
<p>Test strategy, automation, and release confidence without hiring full-time.</p>
</div>
<a class="site-preview" href="https://iliadobkin.com" target="_blank" rel="noopener noreferrer">
<span class="site-preview-bar"><span class="site-preview-url">iliadobkin.com</span><span class="site-preview-go" aria-hidden="true"></span></span>
<span class="site-preview-frame"><img class="site-preview-img" src="./previews/iliadobkin.png" alt="" loading="lazy" decoding="async" /></span>
</a>
</div> </div>
</article> </article>
<article class="folder f6" data-layer="6" id="layer-6"> <article class="folder f6" data-layer="6" id="layer-6">
<button type="button" class="tab" data-goto="6">L6 · interface</button> <button type="button" class="tab" data-goto="6"><span class="tab-code">L6</span><span class="tab-label"> · Source</span></button>
<div class="body"> <div class="body body--has-preview">
<h2>Engage</h2> <div class="body-copy">
<p>Discover → Proposal → Ship → Maintain</p> <h2>Source &amp; repos</h2>
<p><a class="btn" href="https://cal.levkin.ca/ilia/consult">Book 15 min</a> <a class="btn ghost" href="mailto:hello@levkine.ca">hello@levkine.ca</a></p> <p>Project code and templates on our Gitea — private by default, yours on handoff.</p>
</div>
<a class="site-preview" href="https://git.levkin.ca/explore/repos" target="_blank" rel="noopener noreferrer">
<span class="site-preview-bar"><span class="site-preview-url">git.levkin.ca / explore</span><span class="site-preview-go" aria-hidden="true"></span></span>
<span class="site-preview-frame"><img class="site-preview-img" src="./previews/git-repos.png" alt="" loading="lazy" decoding="async" /></span>
</a>
</div> </div>
</article> </article>
<div class="stack-stop"></div> <article class="folder f7 folder--last" data-layer="7" id="layer-7">
</main> <button type="button" class="tab" data-goto="7"><span class="tab-code">L7</span><span class="tab-label"> · Engage</span></button>
<div class="body body--has-preview">
<div class="body-copy">
<h2>Terms &amp; contact</h2>
<ul class="terms-list">
<li><strong>Fixed scope</strong> — quoted after discovery; no open-ended hourly surprises.</li>
<li><strong>Production-ready</strong> — monitoring, failure handling, documentation included.</li>
<li><strong>Handoff-friendly</strong> — your team or the next vendor can maintain what we ship.</li>
</ul>
</div>
<div class="cal-slot is-blocked" data-cal-embed data-cal-theme="light" data-cal-target="cal-inline-l7">
<div class="site-preview-bar"><span class="site-preview-url">cal.levkin.ca · book a call</span><a class="site-preview-go" href="https://cal.levkin.ca/ilia/consult" target="_blank" rel="noopener noreferrer" aria-label="Open calendar in new tab"></a></div>
<div class="cal-embed-frame">
<div class="cal-inline" id="cal-inline-l7"></div>
<a class="cal-embed-fallback" href="https://cal.levkin.ca/ilia/consult" target="_blank" rel="noopener noreferrer">
<img src="./previews/cal-light.png" alt="" decoding="async" />
</a>
</div>
</div>
</div>
</article>
<footer class="foot"><a href="https://git.levkin.ca">git.levkin.ca</a><span>levkin.ca</span></footer> <div class="stack-stop" aria-hidden="true"></div>
</main>
<script type="module"> <script type="module">
import { initStackScroll } from '../shared/stack-scroll.js'; import { initStackScroll } from '../shared/stack-scroll.js';
initStackScroll(); import { initRailRoller } from './folder-rail.js';
import { initCalEmbeds } from './folder-cal.js';
initStackScroll({
sectionSelector: '.folder[data-layer]',
mountSelector: '.mount',
foldTabs: true,
interactionMode: 'navigate',
});
initRailRoller();
initCalEmbeds();
</script> </script>
</body> </body>
</html> </html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Levkin — Stack</title> <title>Levkin — Stack</title>
<meta name="description" content="Levkin — software development, layer by layer." /> <meta name="description" content="Levkin — company specification by layer." />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@ -14,72 +14,70 @@
<body> <body>
<nav class="nav"> <nav class="nav">
<a href="/">← options</a> <a href="/">← options</a>
<span class="variants"><a href="/stack-folder/">folder</a> · <a href="/stack-trace/">trace</a> · <a href="/stack-rack/">rack</a></span> <span class="variants"><a href="/spec/">spec</a> · <a href="/stack-folder/">folder</a></span>
<span class="depth" id="depth">L0</span> <span class="depth" id="depth">L0</span>
</nav> </nav>
<main class="stack-mount"> <main class="stack-mount">
<section class="layer layer-0" data-layer="0"> <section class="layer layer-0" data-layer="0">
<div class="layer-inner"> <div class="layer-inner">
<header class="layer-head"><button type="button" class="layer-id" data-goto="0">L0</button><span class="layer-name">foundation</span></header> <header class="layer-head"><button type="button" class="layer-id" data-goto="0">L0</button><span class="layer-name">company</span></header>
<h1>Levkin</h1> <h1>Levkin Inc.</h1>
<p class="tagline">Software · Canada · remote</p> <p class="tagline">LK-SPEC-1.0 · Software development · Canada</p>
<p class="layer-copy">Boutique engineering — production systems, automation, enterprise.</p> <p class="layer-copy">Builds and maintains production software — custom apps, automation, practice systems. Scoped, documented, handoff-ready.</p>
<div class="chips"><span>15+ yrs</span><span>8h→2m</span><span>24/7</span></div> <p class="avail">ACTIVE · Taking new engagements · remote NA &amp; EU</p>
<p class="avail">Taking new engagements</p>
</div> </div>
</section> </section>
<section class="layer layer-1" data-layer="1"> <section class="layer layer-1" data-layer="1">
<div class="layer-inner"> <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> <header class="layer-head"><button type="button" class="layer-id" data-goto="1">L1</button><span class="layer-name">scope</span></header>
<h2>Custom software</h2> <h2>1. Scope</h2>
<p class="layer-copy">Web apps, APIs, internal tools — TS · Python · .NET</p> <p class="layer-copy">Boutique practice — clear start, deliverable, and handoff. Discovery → fixed proposal → delivery → optional support.</p>
</div> </div>
</section> </section>
<section class="layer layer-2" data-layer="2"> <section class="layer layer-2" data-layer="2">
<div class="layer-inner"> <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> <header class="layer-head"><button type="button" class="layer-id" data-goto="2">L2</button><span class="layer-name">application</span></header>
<h2>Automation</h2> <h2>2.1 Custom software</h2>
<p class="layer-copy">n8n · Zapier · CI/CD · webhooks · LLMs</p> <p class="layer-copy">Web apps, APIs, internal tools — chosen to fit the problem, not a preset stack.</p>
</div> </div>
</section> </section>
<section class="layer layer-3" data-layer="3"> <section class="layer layer-3" data-layer="3">
<div class="layer-inner"> <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> <header class="layer-head"><button type="button" class="layer-id" data-goto="3">L3</button><span class="layer-name">automation</span><a href="https://auto.levkin.ca" class="layer-link">auto.levkin.ca ↗</a></header>
<h2>CaseWare &amp; CaseView</h2> <h2>2.2 Automation</h2>
<p class="layer-copy">15+ years · CaseWare Intl, MNP, JazzIt</p> <p class="layer-copy">Background workflows — reporting, notifications, data sync, ops off your plate.</p>
</div> </div>
</section> </section>
<section class="layer layer-4" data-layer="4"> <section class="layer layer-4" data-layer="4">
<div class="layer-inner"> <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> <header class="layer-head"><button type="button" class="layer-id" data-goto="4">L4</button><span class="layer-name">enterprise</span><a href="https://caseware.levkin.ca" class="layer-link">caseware.levkin.ca</a></header>
<h2>Quality engineering</h2> <h2>2.3 CaseWare</h2>
<p class="layer-copy">Senior SDET · test automation · CI/CD · trace-driven QA</p> <p class="layer-copy">Templates, releases, practice customization — small fixes to full pipeline overhauls.</p>
</div> </div>
</section> </section>
<section class="layer layer-5" data-layer="5"> <section class="layer layer-5" data-layer="5">
<div class="layer-inner"> <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> <header class="layer-head"><button type="button" class="layer-id" data-goto="5">L5</button><span class="layer-name">quality</span><a href="https://iliadobkin.com" class="layer-link">iliadobkin.com</a></header>
<h2>Job Ops</h2> <h2>2.4 Quality &amp; testing</h2>
<p class="layer-copy">Internal hiring orchestrator (auth required)</p> <p class="layer-copy">Test strategy, automation, release confidence — experienced QA lead without full-time hire.</p>
</div> </div>
</section> </section>
<section class="layer layer-6" data-layer="6"> <section class="layer layer-6" data-layer="6">
<div class="layer-inner"> <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> <header class="layer-head"><button type="button" class="layer-id" data-goto="6">L6</button><span class="layer-name">contact</span></header>
<h2>Engage</h2> <h2>Terms &amp; contact</h2>
<p class="layer-copy">Discover → Proposal → Ship → Maintain</p> <p class="layer-copy">Fixed scope · production-ready · handoff-friendly · right-sized.</p>
<div class="contact-row"> <div class="contact-row">
<a class="btn" href="https://cal.levkin.ca/ilia/consult">Book 15 min</a> <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> <a class="btn btn-ghost" href="mailto:ilia@levkine.ca?subject=Project%20enquiry">ilia@levkine.ca</a>
</div> </div>
<p class="guarantees">Retries · docs · tests first</p>
</div> </div>
</section> </section>

View File

@ -36,14 +36,23 @@ body {
margin: 0 auto; margin: 0 auto;
} }
/* Each card sticks; next scrolls over previous */ /* Compact sticky card — click brings to front */
.layer:not(.layer-6) {
margin-bottom: var(--stack-scroll-slot);
}
.layer { .layer {
position: sticky; position: sticky;
min-height: var(--stack-card-min);
margin-bottom: 1.25rem;
border-radius: 8px; border-radius: 8px;
border: 1px solid rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.08);
box-shadow: 0 12px 36px rgba(0,0,0,0.5); box-shadow: 0 12px 36px rgba(0,0,0,0.5);
cursor: pointer;
transition: box-shadow 0.2s;
}
.layer.is-front {
z-index: 100 !important;
box-shadow: 0 16px 48px rgba(0,0,0,0.65);
} }
.layer-0 { top: var(--stack-stick); z-index: 1; background: #1c1c20; } .layer-0 { top: var(--stack-stick); z-index: 1; background: #1c1c20; }
@ -52,9 +61,9 @@ body {
.layer-3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); z-index: 4; background: #343440; } .layer-3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); z-index: 4; background: #343440; }
.layer-4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); z-index: 5; background: #3c3c4a; } .layer-4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); z-index: 5; background: #3c3c4a; }
.layer-5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); z-index: 6; background: #444454; } .layer-5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); z-index: 6; background: #444454; }
.layer-6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); z-index: 7; background: #4c4c5e; margin-bottom: 4rem; } .layer-6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); z-index: 7; background: #4c4c5e; margin-bottom: 0; }
.layer-inner { padding: 1.2rem 1.35rem 1.5rem; } .layer-inner { padding: 0.95rem 1.15rem 1.05rem; }
.layer-head { .layer-head {
display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.65rem; display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.65rem;
@ -70,8 +79,8 @@ body {
.layer-id:hover { text-decoration: underline; } .layer-id:hover { text-decoration: underline; }
.layer-name { color: #6b6966; text-transform: uppercase; letter-spacing: 0.1em; } .layer-name { color: #6b6966; text-transform: uppercase; letter-spacing: 0.1em; }
.layer-link { margin-left: auto; color: #8b9cb3; text-decoration: none; font-size: 0.58rem; } .layer-link { margin-left: auto; color: #8b9cb3; text-decoration: none; font-size: 0.58rem; }
.layer h1 { font-size: 2rem; font-weight: 600; letter-spacing: -0.03em; } .layer h1 { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.03em; }
.layer h2 { font-size: 1.2rem; font-weight: 600; margin-bottom: 0.35rem; } .layer h2 { font-size: 1.05rem; font-weight: 600; margin-bottom: 0.3rem; }
.tagline { font-family: var(--mono); font-size: 0.65rem; color: #6b6966; margin-bottom: 0.4rem; } .tagline { font-family: var(--mono); font-size: 0.65rem; color: #6b6966; margin-bottom: 0.4rem; }
.layer-copy { 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 { display: flex; flex-wrap: wrap; gap: 0.3rem; margin: 0.5rem 0; }
@ -105,6 +114,6 @@ body {
.stack-ruler button.active { color: #c4a574; } .stack-ruler button.active { color: #c4a574; }
@media (max-width: 700px) { @media (max-width: 700px) {
:root { --stack-step: 1.75rem; --stack-card-min: 48vh; } :root { --stack-step: 1.5rem; }
.variants { display: none; } .variants { display: none; }
} }

View File

@ -7,13 +7,8 @@ export default defineConfig({
input: { input: {
main: resolve(__dirname, 'index.html'), main: resolve(__dirname, 'index.html'),
spec: resolve(__dirname, 'spec/index.html'), spec: resolve(__dirname, 'spec/index.html'),
slab: resolve(__dirname, 'slab/index.html'),
relay: resolve(__dirname, 'relay/index.html'),
vault: resolve(__dirname, 'vault/index.html'),
stack: resolve(__dirname, 'stack/index.html'), stack: resolve(__dirname, 'stack/index.html'),
stackFolder: resolve(__dirname, 'stack-folder/index.html'), stackFolder: resolve(__dirname, 'stack-folder/index.html'),
stackTrace: resolve(__dirname, 'stack-trace/index.html'),
stackRack: resolve(__dirname, 'stack-rack/index.html'),
}, },
}, },
}, },