Prune repo to spec and stack-folder only.

Remove cards stack, rack, trace, slab, relay, vault, unused preview
PNGs, and stack-only test scripts. Update home page, README, and Vite inputs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ilia 2026-05-21 21:41:33 -04:00
parent 21c75cdcba
commit 85229b09c8
26 changed files with 31 additions and 2005 deletions

View File

@ -1,79 +1,64 @@
# levkin.ca # levkin.ca
Design concepts for the Levkin software development company homepage. Homepage design concepts for Levkin Inc.
| Option | Path | Vibe | | Page | Path | Description |
|--------|------|------| |------|------|-------------|
| **Spec** | `/spec/` | RFC documentation, endpoints, iliadobkin.com | | **Spec** | `/spec/` | Company story as RFC-style documentation |
| **Cards** | `/stack/` | Dark overlapping sticky cards — click a layer to bring it forward | | **Folder** | `/stack-folder/` | Manila folders L0L7, sticky tab stack, site previews |
| **Folder** | `/stack-folder/` | Manila folders, staggered tabs (L0L7), site previews |
Open `/` to compare all three. Open `/` to pick a direction.
## Develop ## Develop
```bash ```bash
cd ~/Documents/code/levkin.ca
npm install 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`. Vite default: `http://localhost:5173`
| Page | URL | | Page | URL |
|------|-----| |------|-----|
| Compare | `/` | | Home | `/` |
| Spec | `/spec/` | | Spec | `/spec/` |
| Cards | `/stack/` |
| Folders | `/stack-folder/` | | Folders | `/stack-folder/` |
## Build ## Build
```bash ```bash
npm run build npm run build
# output in dist/ # dist/
``` ```
## Folder site (`/stack-folder/`) ## Folder stack (`/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`). Eight layers with labeled tabs. Scroll pins earlier tabs on a shared rail; L7 joins last. Scroll stops when all tabs align.
- **L0** — Company + Cal embed - `shared/stack-scroll.js` — scroll depth, fold state, layer jump
- **L1** — Scope + spec preview - `stack-folder/folder-rail.js` — side rail
- **L2** — Services - `stack-folder/folder-cal.js` — Cal.com embeds
- **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 ### Preview screenshots
Each linked folder shows a screenshot; refresh captures with dev server running:
```bash ```bash
npm run dev # separate terminal npm run dev # separate terminal
npm run capture-previews npm run capture-previews
``` ```
Writes PNGs to `stack-folder/previews/`. Writes PNGs used in `stack-folder/previews/` (spec, auto, caseware, iliadobkin, git-repos, cal dark/light).
### Tests (Playwright) ### Test
Requires dev server on the URL you pass:
```bash ```bash
npm run dev npm run dev
STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder 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)
- [caseware.levkin.ca](https://caseware.levkin.ca) — CaseWare consulting - [caseware.levkin.ca](https://caseware.levkin.ca)
- [jobs.levkin.ca](https://jobs.levkin.ca) — job orchestration - [git.levkin.ca](https://git.levkin.ca)
- [git.levkin.ca](https://git.levkin.ca) — source control - [cal.levkin.ca](https://cal.levkin.ca/ilia/consult)
- [iliadobkin.com](https://iliadobkin.com) — SDET portfolio · quality engineering - [iliadobkin.com](https://iliadobkin.com)

View File

@ -24,7 +24,7 @@
min-height: 100vh; min-height: 100vh;
line-height: 1.5; line-height: 1.5;
} }
.wrap { max-width: 900px; margin: 0 auto; padding: 4rem 1.5rem 6rem; } .wrap { max-width: 720px; 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,7 +44,7 @@
.grid { .grid {
display: grid; display: grid;
gap: 1.25rem; gap: 1.25rem;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
} }
a.card { a.card {
display: block; display: block;
@ -72,22 +72,6 @@
font-family: 'DM Mono', monospace; font-family: 'DM Mono', monospace;
letter-spacing: 0.06em; letter-spacing: 0.06em;
} }
.preview--stack {
background: #0e0e10;
flex-direction: column;
gap: 4px;
padding: 1.5rem;
}
.preview--stack .card-layer {
width: 70%;
height: 12px;
border-radius: 3px;
border: 1px solid rgba(255,255,255,0.1);
background: linear-gradient(90deg, #2a2a32, #3a3a46);
}
.preview--stack .card-layer:nth-child(1) { width: 55%; opacity: 0.5; }
.preview--stack .card-layer:nth-child(2) { width: 62%; opacity: 0.7; }
.preview--stack .card-layer:nth-child(3) { width: 75%; background: #4a4a58; }
.preview--folder { .preview--folder {
background: #e8e2d4; background: #e8e2d4;
color: #2a2824; color: #2a2824;
@ -121,8 +105,8 @@
<div class="wrap"> <div class="wrap">
<header> <header>
<p class="eyebrow">levkin.ca</p> <p class="eyebrow">levkin.ca</p>
<h1>Three directions.</h1> <h1>Two directions.</h1>
<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> <p class="lead">Spec for the company story. Folder for the L0L7 scroll stack with site previews.</p>
</header> </header>
<div class="grid"> <div class="grid">
@ -135,29 +119,18 @@
</div> </div>
</a> </a>
<a class="card" href="/stack/">
<div class="preview preview--stack">
<span class="card-layer"></span><span class="card-layer"></span><span class="card-layer"></span>
</div>
<div class="card-body">
<h2>Cards</h2>
<p>Dark sticky stack. Scroll or click L0L6 to focus a layer.</p>
<span class="tag">stack · scroll</span>
</div>
</a>
<a class="card" href="/stack-folder/"> <a class="card" href="/stack-folder/">
<div class="preview preview--folder">L0│ L1│ L2│</div> <div class="preview preview--folder">L0 · L1 · L2 · … L7</div>
<div class="card-body"> <div class="card-body">
<h2>Folder</h2> <h2>Folder</h2>
<p>Spec as a filing cabinet — manila tabs, clause sections, pull a file forward.</p> <p>Manila tabs, sticky stack, previews of related sites.</p>
<span class="tag">folder · spec</span> <span class="tag">folder · scroll</span>
</div> </div>
</a> </a>
</div> </div>
<footer> <footer>
<p><a href="https://git.levkin.ca">git.levkin.ca</a> · <code>cd ~/Documents/code/levkin.ca && npm run dev</code></p> <p><a href="https://git.levkin.ca">git.levkin.ca</a> · <code>npm run dev</code></p>
</footer> </footer>
</div> </div>
</body> </body>

View File

@ -8,8 +8,7 @@
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"capture-previews": "node scripts/capture-previews.mjs", "capture-previews": "node scripts/capture-previews.mjs",
"test:folder": "node scripts/test-stack-folder.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

@ -1,94 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Levkin — Relay</title>
<meta name="description" content="Levkin — software development. Signals in, production out." />
<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 />
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./relay.css" />
</head>
<body>
<div class="grain" aria-hidden="true"></div>
<nav class="nav">
<a href="/">← options</a>
<span class="station">STN · LEV-01</span>
</nav>
<main>
<header class="header">
<h1>Levkin Relay</h1>
<p class="sub">Software development · signal in · production out</p>
</header>
<section class="tape" aria-label="Incoming transmissions">
<div class="tape-head">
<span>INCOMING</span>
<button type="button" id="decode-btn" class="decode-btn">Decode ↵</button>
</div>
<pre class="morse" id="morse" aria-live="polite"></pre>
<p class="decoded hidden" id="decoded"></p>
</section>
<section class="channels">
<h2>Active lines</h2>
<ul>
<li>
<span class="line-id">L-1</span>
<span class="line-name">Custom software</span>
<span class="line-desc">TS · Python · .NET · APIs</span>
</li>
<li>
<span class="line-id">L-2</span>
<span class="line-name">Automation</span>
<a href="https://auto.levkin.ca" class="line-link">auto.levkin.ca</a>
</li>
<li>
<span class="line-id">L-3</span>
<span class="line-name">CaseWare</span>
<a href="https://caseware.levkin.ca" class="line-link">caseware.levkin.ca</a>
</li>
<li>
<span class="line-id">L-4</span>
<span class="line-name">Job Ops</span>
<a href="https://jobs.levkin.ca" class="line-link">jobs.levkin.ca</a>
</li>
<li>
<span class="line-id">L-5</span>
<span class="line-name">SDET / QA</span>
<a href="https://iliadobkin.com" class="line-link">iliadobkin.com</a>
</li>
</ul>
</section>
<section class="metrics">
<div class="metric"><strong>15+</strong> yrs enterprise</div>
<div class="metric"><strong>8h→2m</strong> releases</div>
<div class="metric"><strong>open</strong> engagements</div>
</section>
<section class="principles">
<p>Every transmission handled with retries, documentation, and tests before it reaches production. Canadian · remote NA &amp; EU · fixed-scope quotes after discovery.</p>
<p class="clients">CaseWare International · MNP · JazzIt</p>
</section>
<section class="send">
<p class="send-label">Schedule transmission:</p>
<a href="https://cal.levkin.ca/ilia/consult" class="send-addr send-cal">15 min consultation →</a>
<p class="send-label">Or reply to:</p>
<a href="mailto:hello@levkine.ca?subject=Relay%20enquiry" class="send-addr">hello@levkine.ca</a>
</section>
</main>
<footer>
<a href="https://git.levkin.ca">git.levkin.ca</a>
<span>levkin.ca</span>
</footer>
<script src="./relay.js" type="module"></script>
</body>
</html>

View File

@ -1,240 +0,0 @@
:root {
--bg: #1a1814;
--paper: #e8e0d4;
--amber: #d4a574;
--dim: #6b6358;
--mono: 'Courier Prime', 'Courier New', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--mono);
background: var(--bg);
color: var(--paper);
min-height: 100vh;
line-height: 1.55;
}
.grain {
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.04;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
z-index: 0;
}
.nav {
position: relative;
z-index: 2;
display: flex;
justify-content: space-between;
padding: 1.25rem 2rem;
font-size: 0.75rem;
letter-spacing: 0.1em;
}
.nav a {
color: var(--dim);
text-decoration: none;
}
.nav a:hover { color: var(--amber); }
.station { color: var(--amber); }
main {
position: relative;
z-index: 1;
max-width: 560px;
margin: 0 auto;
padding: 2rem 2rem 4rem;
}
.header {
margin-bottom: 2.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid rgba(212, 165, 116, 0.25);
}
.header h1 {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--amber);
margin-bottom: 0.5rem;
}
.sub {
font-size: 0.8rem;
color: var(--dim);
letter-spacing: 0.05em;
}
.tape {
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(212, 165, 116, 0.2);
padding: 1.25rem;
margin-bottom: 2.5rem;
}
.tape-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
font-size: 0.65rem;
letter-spacing: 0.2em;
color: var(--amber);
}
.decode-btn {
font-family: var(--mono);
font-size: 0.7rem;
background: transparent;
border: 1px solid var(--amber);
color: var(--amber);
padding: 0.35rem 0.65rem;
cursor: pointer;
letter-spacing: 0.08em;
}
.decode-btn:hover {
background: rgba(212, 165, 116, 0.15);
}
.morse {
font-size: 0.85rem;
color: var(--dim);
word-break: break-all;
line-height: 1.8;
min-height: 3rem;
}
.decoded {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px dashed rgba(212, 165, 116, 0.3);
font-size: 0.95rem;
color: var(--paper);
line-height: 1.6;
}
.decoded.hidden { display: none; }
.channels { margin-bottom: 2rem; }
.channels h2 {
font-size: 0.65rem;
letter-spacing: 0.25em;
color: var(--dim);
margin-bottom: 1rem;
}
.channels ul { list-style: none; }
.line-desc {
font-size: 0.7rem;
color: var(--dim);
grid-column: 2;
}
.channels li {
display: grid;
grid-template-columns: 2.5rem 1fr auto;
gap: 0.25rem 0.5rem;
align-items: center;
padding: 0.65rem 0;
border-bottom: 1px solid rgba(212, 165, 116, 0.1);
font-size: 0.85rem;
}
.line-id { color: var(--dim); font-size: 0.75rem; }
.line-status { color: var(--amber); font-size: 0.7rem; letter-spacing: 0.1em; }
.line-link {
color: var(--amber);
text-decoration: none;
font-size: 0.75rem;
}
.line-link:hover { text-decoration: underline; }
.metrics {
display: flex;
flex-wrap: wrap;
gap: 1rem 1.5rem;
margin-bottom: 1.5rem;
padding: 1rem 0;
border-top: 1px solid rgba(212, 165, 116, 0.15);
border-bottom: 1px solid rgba(212, 165, 116, 0.15);
}
.metric {
font-size: 0.75rem;
letter-spacing: 0.06em;
color: var(--dim);
}
.metric strong {
color: var(--amber);
font-weight: 700;
}
.principles {
font-size: 0.85rem;
color: var(--dim);
margin-bottom: 2.5rem;
line-height: 1.65;
}
.principles .clients {
margin-top: 0.75rem;
font-size: 0.75rem;
letter-spacing: 0.1em;
color: var(--amber);
opacity: 0.8;
}
.send {
padding: 1.5rem 0;
border-top: 1px solid rgba(212, 165, 116, 0.25);
}
.send-label {
font-size: 0.65rem;
letter-spacing: 0.2em;
color: var(--dim);
margin-bottom: 0.5rem;
margin-top: 1rem;
}
.send-label:first-child { margin-top: 0; }
.send-addr {
font-size: 1.1rem;
color: var(--amber);
text-decoration: none;
letter-spacing: 0.05em;
}
.send-addr:hover { text-decoration: underline; }
footer {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
padding: 1.5rem 2rem;
font-size: 0.7rem;
color: var(--dim);
letter-spacing: 0.08em;
}
footer a {
color: var(--dim);
text-decoration: none;
}
footer a:hover { color: var(--amber); }

View File

@ -1,66 +0,0 @@
const MESSAGES = [
{
morse: '·−·−− · ·−·−− ·−· · ·−·−− · ·−·−− ·−· · ·−·−− ·−−−',
text: 'Levkin — Canadian software practice. Custom apps, automation (n8n, CI/CD, LLMs), CaseWare, SDET. Remote NA & EU. Taking new work.',
},
{
morse: ' ·−·−− · ·−·−− ·−· · ·−·−− ·−−− · ·−·−− ·−· · ·−·−− ',
text: 'Proof: 15+ years enterprise. CaseWare releases cut from 8 hours to under 2 minutes. Automation runs 24/7 — not demos.',
},
{
morse: '·−·−− · ·−·−− ·−· · ·−·−− · ·−·−− ·−· · ·−·−− ·−−−',
text: 'Discover (15 min) → Proposal (fixed scope) → Ship (tested, documented) → Maintain (optional). auto.levkin.ca · caseware.levkin.ca · iliadobkin.com',
},
{
morse: ' ·−·−− · ·−·−− ·−· · ·−·−− ·−−− · ·−·−− ·−· · ·−·−− · ·−·−− ·−· · ·−·−− ·−−−',
text: 'Error handling required. Documentation required. Tests before live data. Pragmatism: 20-line script beats 200-node workflow when it fits.',
},
];
const morseEl = document.getElementById('morse');
const decodedEl = document.getElementById('decoded');
const btn = document.getElementById('decode-btn');
let idx = 0;
let typing = false;
function typeMorse(text, cb) {
typing = true;
morseEl.textContent = '';
let i = 0;
const tick = () => {
if (i < text.length) {
morseEl.textContent += text[i];
i++;
setTimeout(tick, 35 + Math.random() * 40);
} else {
typing = false;
cb?.();
}
};
tick();
}
function showMessage() {
const msg = MESSAGES[idx % MESSAGES.length];
decodedEl.classList.add('hidden');
typeMorse(msg.morse, () => {
btn.disabled = false;
btn.textContent = 'Decode ↵';
});
btn._pending = msg;
}
btn.addEventListener('click', () => {
if (typing) return;
if (btn._pending && decodedEl.classList.contains('hidden')) {
decodedEl.textContent = btn._pending.text;
decodedEl.classList.remove('hidden');
btn.textContent = 'Next signal →';
btn._pending = null;
return;
}
idx++;
showMessage();
});
showMessage();

View File

@ -13,7 +13,6 @@ mkdirSync(out, { recursive: true });
const shots = [ const shots = [
['spec', `${base}/spec/`], ['spec', `${base}/spec/`],
['stack', `${base}/stack/`],
['auto', 'https://auto.levkin.ca'], ['auto', 'https://auto.levkin.ca'],
['caseware', 'https://caseware.levkin.ca'], ['caseware', 'https://caseware.levkin.ca'],
['iliadobkin', 'https://iliadobkin.com'], ['iliadobkin', 'https://iliadobkin.com'],

View File

@ -1,34 +0,0 @@
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:5173/stack-folder/', { waitUntil: 'networkidle' });
for (const y of [0, 400, 800, 1200]) {
await page.evaluate((sy) => window.scrollTo(0, sy), y);
await page.waitForTimeout(150);
const data = await page.evaluate(() => {
return [...document.querySelectorAll('.folder')].map((f, i) => {
const cs = getComputedStyle(f);
const r = f.getBoundingClientRect();
const body = f.querySelector('.body');
const br = body.getBoundingClientRect();
const sec = f.closest('.scroll-section');
const sr = sec.getBoundingClientRect();
return {
i,
folderTop: Math.round(r.top),
bodyTop: Math.round(br.top),
position: cs.position,
top: cs.top,
zIndex: f.style.zIndex || cs.zIndex,
sectionTop: Math.round(sr.top),
sectionBottom: Math.round(sr.bottom),
sectionH: Math.round(sr.height),
};
});
});
console.log('scroll', y, JSON.stringify(data.filter((d) => d.sectionBottom > 0 && d.sectionTop < 800), null, 2));
}
await browser.close();

View File

@ -1,50 +0,0 @@
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();

View File

@ -1,118 +0,0 @@
/**
* 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,76 +0,0 @@
import { chromium } from 'playwright';
import { writeFileSync, mkdirSync } from 'fs';
import { join } from 'path';
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/', 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, 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 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),
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, bodySel } of pages) {
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
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(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, shots });
await page.close();
}
await browser.close();
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));

View File

@ -1,87 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Levkin</title>
<meta name="description" content="Levkin — software development." />
<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 />
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Archivo:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./slab.css" />
</head>
<body>
<a href="/" class="back"></a>
<main>
<header class="mast">
<span class="year">EST · CA</span>
<h1>LEV<br />KIN</h1>
<p class="role">SOFTWARE DEVELOPMENT</p>
</header>
<section class="strip strip-red">
<p>WE BUILD WHAT RUNS.</p>
</section>
<section class="strip strip-avail">
<p>AVAILABLE · NEW ENGAGEMENTS · REMOTE NA &amp; EU</p>
</section>
<section class="stats">
<div class="stat"><span class="stat-n">15+</span><span class="stat-l">YEARS</span></div>
<div class="stat"><span class="stat-n">8h→2m</span><span class="stat-l">RELEASES</span></div>
<div class="stat"><span class="stat-n">24/7</span><span class="stat-l">AUTOMATION</span></div>
</section>
<section class="blocks">
<div class="block">
<span class="num">01</span>
<h2>CUSTOM</h2>
<p>Apps · APIs · TS · Python · .NET</p>
</div>
<div class="block block-inv">
<span class="num">02</span>
<h2>AUTO</h2>
<p>n8n · Zapier · CI/CD · LLMs</p>
<p class="block-link"><a href="https://auto.levkin.ca">auto.levkin.ca</a></p>
</div>
<div class="block">
<span class="num">03</span>
<h2>CASE</h2>
<p>CaseWare · MNP · JazzIt</p>
<p class="block-link"><a href="https://caseware.levkin.ca">caseware.levkin.ca</a></p>
</div>
<div class="block block-inv">
<span class="num">04</span>
<h2>OPS</h2>
<p>Job orchestration · auth</p>
<p class="block-link"><a href="https://jobs.levkin.ca">jobs.levkin.ca</a></p>
</div>
<div class="block block-wide">
<span class="num">05</span>
<h2>QE</h2>
<p>Senior SDET · test automation · traces</p>
<p class="block-link"><a href="https://iliadobkin.com">iliadobkin.com</a></p>
</div>
</section>
<section class="strip">
<ul class="rules">
<li>PRODUCTION READY</li>
<li>DOCUMENTED</li>
<li>TESTED FIRST</li>
<li>NO SCOPE CREEP</li>
</ul>
</section>
<section class="cta-block">
<a href="https://cal.levkin.ca/ilia/consult" class="cta cta-primary">BOOK 15 MIN →</a>
<a href="mailto:hello@levkine.ca?subject=Project%20enquiry" class="cta">HELLO@LEVGINE.CA →</a>
</section>
</main>
<footer>LEVKIN.CA · <a href="https://git.levkin.ca">GIT</a></footer>
</body>
</html>

View File

@ -1,240 +0,0 @@
:root {
--white: #f5f5f0;
--black: #0a0a0a;
--red: #e63946;
--font-display: 'Archivo Black', system-ui, sans-serif;
--font: 'Archivo', system-ui, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font);
background: var(--white);
color: var(--black);
min-height: 100vh;
}
.back {
position: fixed;
top: 1.25rem;
left: 1.25rem;
z-index: 10;
font-size: 1.25rem;
color: var(--black);
text-decoration: none;
font-weight: 500;
}
main { max-width: 100%; }
.mast {
padding: 4rem 1.5rem 3rem;
border-bottom: 6px solid var(--black);
}
.year {
font-size: 0.7rem;
letter-spacing: 0.25em;
font-weight: 500;
display: block;
margin-bottom: 2rem;
}
.mast h1 {
font-family: var(--font-display);
font-size: clamp(4rem, 18vw, 11rem);
line-height: 0.85;
letter-spacing: -0.04em;
font-weight: 400;
}
.role {
margin-top: 1.5rem;
font-size: 0.8rem;
letter-spacing: 0.35em;
font-weight: 500;
}
.strip {
padding: 1.25rem 1.5rem;
border-bottom: 4px solid var(--black);
}
.strip-red {
background: var(--red);
color: var(--white);
}
.strip-red p {
font-family: var(--font-display);
font-size: clamp(1.25rem, 4vw, 2rem);
letter-spacing: 0.02em;
}
.strip-avail {
background: var(--black);
color: var(--white);
}
.strip-avail p {
font-size: 0.7rem;
letter-spacing: 0.2em;
font-weight: 500;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
border-bottom: 4px solid var(--black);
}
.stat {
padding: 1.5rem 1rem;
text-align: center;
border-right: 4px solid var(--black);
}
.stat:last-child { border-right: none; }
.stat-n {
display: block;
font-family: var(--font-display);
font-size: 1.75rem;
letter-spacing: -0.02em;
}
.stat-l {
font-size: 0.6rem;
letter-spacing: 0.25em;
font-weight: 500;
}
.block-link {
margin-top: 0.35rem;
font-size: 0.75rem;
}
.block-link a {
color: inherit;
text-decoration: underline;
text-underline-offset: 2px;
}
.blocks {
display: grid;
grid-template-columns: 1fr 1fr;
}
@media (max-width: 500px) {
.blocks { grid-template-columns: 1fr; }
}
.block {
padding: 2rem 1.5rem;
border-bottom: 4px solid var(--black);
border-right: 4px solid var(--black);
min-height: 160px;
}
.block:nth-child(2n) { border-right: none; }
.block-wide {
grid-column: 1 / -1;
border-right: none;
}
.block-inv {
background: var(--black);
color: var(--white);
}
.num {
font-size: 0.65rem;
letter-spacing: 0.2em;
opacity: 0.5;
display: block;
margin-bottom: 0.75rem;
}
.block h2 {
font-family: var(--font-display);
font-size: 2rem;
letter-spacing: -0.02em;
margin-bottom: 0.35rem;
}
.block p {
font-size: 0.85rem;
letter-spacing: 0.05em;
}
.block a {
color: inherit;
text-decoration: underline;
text-underline-offset: 3px;
}
.rules {
list-style: none;
display: grid;
gap: 0.5rem;
}
.rules li {
font-family: var(--font-display);
font-size: clamp(0.9rem, 2.5vw, 1.1rem);
letter-spacing: 0.08em;
}
.cta-block {
padding: 3rem 1.5rem;
border-bottom: 6px solid var(--black);
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.cta {
font-family: var(--font-display);
font-size: clamp(1rem, 3vw, 1.5rem);
color: var(--black);
text-decoration: none;
letter-spacing: 0.04em;
border-bottom: 4px solid var(--red);
padding-bottom: 0.25rem;
transition: color 0.15s, border-color 0.15s;
}
.cta:hover {
color: var(--red);
border-color: var(--black);
}
.cta-primary {
background: var(--black);
color: var(--white);
padding: 0.5rem 0;
border-bottom-color: var(--black);
}
.cta-primary:hover {
color: var(--red);
background: var(--black);
}
footer {
padding: 1.25rem 1.5rem;
font-size: 0.7rem;
letter-spacing: 0.2em;
font-weight: 500;
}
footer a {
color: var(--black);
text-decoration: none;
}
footer a:hover { text-decoration: underline; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

View File

@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Levkin — Stack Rack</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./rack.css" />
</head>
<body>
<nav class="nav"><a href="/">← options</a><a href="/stack-trace/">trace</a><a href="/stack-folder/">folder</a><span id="depth">U0</span></nav>
<div class="rack-frame">
<div class="rack-label">LEV-RACK-01</div>
<main class="mount">
<article class="unit u0" data-layer="0">
<header class="unit-head"><button type="button" class="jump" data-goto="0"><span class="led on"></span><span class="uid">U0</span><span class="svc">foundation</span></button></header>
<div class="unit-body"><strong>Levkin</strong><p>Software dev · CA · 15+ yrs</p></div>
</article>
<article class="unit u1" data-layer="1">
<header class="unit-head"><button type="button" class="jump" data-goto="1"><span class="led on"></span><span class="uid">U1</span><span class="svc">application</span></button></header>
<div class="unit-body"><strong>custom_software</strong><p>TS · Python · .NET</p></div>
</article>
<article class="unit u2" data-layer="2">
<header class="unit-head"><button type="button" class="jump" data-goto="2"><span class="led on"></span><span class="uid">U2</span><span class="svc">automation</span></button><a class="ext" href="https://auto.levkin.ca">auto↗</a></header>
<div class="unit-body"><strong>pipeline</strong><p>n8n · CI/CD</p></div>
</article>
<article class="unit u3" data-layer="3">
<header class="unit-head"><button type="button" class="jump" data-goto="3"><span class="led on"></span><span class="uid">U3</span><span class="svc">enterprise</span></button><a class="ext" href="https://caseware.levkin.ca">case↗</a></header>
<div class="unit-body"><strong>caseware</strong><p>MNP · JazzIt</p></div>
</article>
<article class="unit u4" data-layer="4">
<header class="unit-head"><button type="button" class="jump" data-goto="4"><span class="led blink"></span><span class="uid">U4</span><span class="svc">quality</span></button><a class="ext" href="https://iliadobkin.com">sdet↗</a></header>
<div class="unit-body"><strong>sdet_suite</strong><p>iliadobkin.com</p></div>
</article>
<article class="unit u5" data-layer="5">
<header class="unit-head"><button type="button" class="jump" data-goto="5"><span class="led"></span><span class="uid">U5</span><span class="svc">ops</span></button><a class="ext" href="https://jobs.levkin.ca">jobs↗</a></header>
<div class="unit-body"><strong>job_ops</strong><p>auth</p></div>
</article>
<article class="unit u6" data-layer="6">
<header class="unit-head"><button type="button" class="jump" data-goto="6"><span class="led on"></span><span class="uid">U6</span><span class="svc">interface</span></button></header>
<div class="unit-body"><strong>engage()</strong><p><a href="https://cal.levkin.ca/ilia/consult">book</a> · <a href="mailto:hello@levkine.ca">mail</a></p></div>
</article>
<div class="stop"></div>
</main>
</div>
<footer class="foot"><span>// rack EOT</span><a href="https://git.levkin.ca">git.levkin.ca</a></footer>
<script type="module">
import { initStackScroll } from '../shared/stack-scroll.js';
initStackScroll({ depthPrefix: 'U' });
</script>
</body>
</html>

View File

@ -1,110 +0,0 @@
@import '../shared/stack-vars.css';
@import '../shared/stack-layout.css';
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'IBM Plex Mono', ui-monospace, monospace;
background: #08090c;
color: #9ca3af;
font-size: 0.75rem;
}
.nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 200;
display: flex; gap: 1rem; padding: 0.5rem 1rem; font-size: 0.62rem;
background: rgba(8,9,12,0.95); border-bottom: 1px solid #1e2430;
}
.nav a { color: #5a6478; text-decoration: none; }
.nav a:hover { color: #4ade80; }
#depth { margin-left: auto; color: #4ade80; font-weight: 600; }
.rack-frame {
width: min(600px, 100%);
margin: 0 auto;
padding: var(--stack-nav) 0.5rem 0;
border-left: 4px solid #2a3040;
border-right: 4px solid #2a3040;
background: linear-gradient(90deg, #0c0e14 0%, #12141a 8%, #12141a 92%, #0c0e14 100%);
}
.rack-label {
font-size: 0.55rem; letter-spacing: 0.2em; color: #3a4458;
padding: 0 0.5rem 0.65rem; border-bottom: 1px dashed #2a3040;
}
.mount { padding: 0 0.25rem; }
.unit {
position: sticky;
min-height: var(--stack-card-min);
margin-bottom: 1.25rem;
border: 1px solid #2a3448;
background: #161a22;
border-radius: 2px;
box-shadow: 0 6px 0 #0a0c10, 0 12px 24px rgba(0,0,0,0.4);
}
.unit-head {
position: sticky;
display: flex; align-items: center; gap: 0.5rem;
padding: 0.4rem 0.65rem;
background: #1a2030;
border-bottom: 1px solid #2a3448;
z-index: 80;
}
.unit-head button.jump {
background: none; border: none; font: inherit; color: inherit;
cursor: pointer; display: flex; align-items: center; gap: 0.5rem; flex: 1;
text-align: left; padding: 0;
}
.unit-head button.jump:hover .uid { color: #4ade80; }
.led { width: 6px; height: 6px; border-radius: 50%; background: #3a4458; flex-shrink: 0; }
.led.on { background: #4ade80; box-shadow: 0 0 6px #4ade80; }
.led.blink { background: #fbbf24; animation: blink 1.2s ease infinite; }
@keyframes blink { 50% { opacity: 0.35; } }
.uid { color: #6b7280; font-weight: 600; min-width: 1.4rem; }
.svc { color: #9ca3af; text-transform: uppercase; letter-spacing: 0.08em; flex: 1; }
.unit-head a.ext { color: #60a5fa; text-decoration: none; font-size: 0.58rem; }
.unit-body {
padding: 0.6rem 0.7rem 1.5rem;
background: #161a22;
}
.u0 { top: var(--stack-stick); z-index: 1; }
.u0 .unit-head { top: var(--stack-stick); }
.u1 { top: calc(var(--stack-stick) + var(--stack-step)); z-index: 2; }
.u1 .unit-head { top: calc(var(--stack-stick) + var(--stack-step)); }
.u2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); z-index: 3; }
.u2 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
.u3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); z-index: 4; }
.u3 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
.u4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); z-index: 5; }
.u4 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 4); }
.u5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); z-index: 6; }
.u5 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
.u6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); z-index: 7; margin-bottom: 4rem; }
.u6 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 6); }
.unit-body strong { color: #e5e7eb; display: block; margin-bottom: 0.2rem; }
.unit-body p { color: #6b7280; font-size: 0.7rem; }
.unit-body a { color: #60a5fa; text-decoration: none; }
.foot {
display: flex; justify-content: space-between;
width: min(600px, 100%); margin: 0 auto;
padding: 1rem 1.25rem 2.5rem; font-size: 0.58rem; color: #3a4458;
}
.foot a { color: #3a4458; text-decoration: none; }

View File

@ -1,50 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Levkin — Stack Trace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./trace.css" />
</head>
<body>
<nav class="nav"><a href="/">← options</a><a href="/stack/">cards</a><a href="/stack-folder/">folder</a><a href="/stack-rack/">rack</a><span id="depth">#0</span></nav>
<main class="mount">
<article class="frame f0" data-layer="0">
<button type="button" class="frame-line" data-goto="0">at Levkin.foundation (levkin.ca:0)</button>
<div class="frame-body"><strong>Levkin</strong><p>Canadian software practice · 15+ yrs · remote</p></div>
</article>
<article class="frame f1" data-layer="1">
<button type="button" class="frame-line" data-goto="1">at Levkin.application (levkin.ca:1)</button>
<div class="frame-body"><strong>custom_software()</strong><p>TS · Python · .NET · APIs</p></div>
</article>
<article class="frame f2" data-layer="2">
<button type="button" class="frame-line" data-goto="2">at Levkin.automation (auto.levkin.ca:2)</button>
<div class="frame-body"><strong>automation_pipeline()</strong><p>n8n · CI/CD · LLMs</p></div>
</article>
<article class="frame f3" data-layer="3">
<button type="button" class="frame-line" data-goto="3">at Levkin.caseware (caseware.levkin.ca:3)</button>
<div class="frame-body"><strong>enterprise_module()</strong><p>CaseWare · MNP · JazzIt</p></div>
</article>
<article class="frame f4" data-layer="4">
<button type="button" class="frame-line" data-goto="4">at Levkin.quality (iliadobkin.com:4)</button>
<div class="frame-body"><strong>sdet_suite()</strong><p><a href="https://iliadobkin.com">iliadobkin.com</a> — traces · Playwright</p></div>
</article>
<article class="frame f5" data-layer="5">
<button type="button" class="frame-line" data-goto="5">at Levkin.operations (jobs.levkin.ca:5)</button>
<div class="frame-body"><strong>job_ops()</strong><p>internal · auth required</p></div>
</article>
<article class="frame f6" data-layer="6">
<button type="button" class="frame-line" data-goto="6">at Levkin.engage (interface:6)</button>
<div class="frame-body"><strong>main()</strong><p><a href="https://cal.levkin.ca/ilia/consult">book_consult()</a> · <a href="mailto:hello@levkine.ca">send_mail()</a></p></div>
</article>
<div class="stop"></div>
</main>
<footer class="foot"><span>// end of stack</span><a href="https://git.levkin.ca">git.levkin.ca</a></footer>
<script type="module">
import { initStackScroll } from '../shared/stack-scroll.js';
initStackScroll({ depthPrefix: '#' });
</script>
</body>
</html>

View File

@ -1,90 +0,0 @@
@import '../shared/stack-vars.css';
@import '../shared/stack-layout.css';
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'IBM Plex Mono', ui-monospace, monospace;
background: #0d0d0f;
color: #b8b4af;
font-size: 0.8rem;
}
.nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 200;
display: flex; gap: 1rem; padding: 0.5rem 1rem; font-size: 0.62rem;
background: rgba(13,13,15,0.95); border-bottom: 1px solid #2a2a30;
}
.nav a { color: #5a5854; text-decoration: none; }
.nav a:hover { color: #7eb87a; }
#depth { margin-left: auto; color: #7eb87a; font-weight: 600; }
.mount {
width: min(600px, 100%);
margin: 0 auto;
padding-top: var(--stack-nav);
}
.frame {
position: sticky;
min-height: var(--stack-card-min);
margin: 0 0.5rem 1.25rem;
border-left: 3px solid #3a3a44;
background: #141418;
box-shadow: 0 8px 0 #0a0a0c, 0 14px 28px rgba(0,0,0,0.45);
}
.frame-line {
position: sticky;
display: block;
font-size: 0.66rem;
color: #6b9b6b;
border: none;
font-family: inherit;
cursor: pointer;
text-align: left;
width: 100%;
padding: 0.65rem 0 0.35rem 1rem;
background: #141418;
z-index: 80;
}
.frame-line:hover { color: #9fdf9f; text-decoration: underline; }
.frame-body {
padding: 0 0 1.5rem 1rem;
background: #141418;
}
.f0 { top: var(--stack-stick); z-index: 1; border-color: #c4a574; }
.f0 .frame-line { top: var(--stack-stick); }
.f1 { top: calc(var(--stack-stick) + var(--stack-step)); z-index: 2; }
.f1 .frame-line { top: calc(var(--stack-stick) + var(--stack-step)); }
.f2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); z-index: 3; }
.f2 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
.f3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); z-index: 4; }
.f3 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
.f4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); z-index: 5; border-color: #6b8b9b; }
.f4 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 4); }
.f5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); z-index: 6; }
.f5 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
.f6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); z-index: 7; border-color: #7eb87a; margin-bottom: 4rem; }
.f6 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 6); }
.frame-body strong { color: #e8e6e3; font-weight: 500; display: block; margin-bottom: 0.2rem; }
.frame-body p { color: #6b6966; font-size: 0.74rem; }
.frame-body a { color: #8b9cb3; text-decoration: none; }
.foot {
display: flex; justify-content: space-between;
width: min(600px, 100%); margin: 0 auto;
padding: 0 1.5rem 2.5rem; font-size: 0.6rem; color: #3a3a40;
}
.foot a { color: #3a3a40; text-decoration: none; }

View File

@ -1,102 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Levkin — Stack</title>
<meta name="description" content="Levkin — company specification by layer." />
<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 />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./stack.css" />
</head>
<body>
<nav class="nav">
<a href="/">← options</a>
<span class="variants"><a href="/spec/">spec</a> · <a href="/stack-folder/">folder</a></span>
<span class="depth" id="depth">L0</span>
</nav>
<main class="stack-mount">
<section class="layer layer-0" data-layer="0">
<div class="layer-inner">
<header class="layer-head"><button type="button" class="layer-id" data-goto="0">L0</button><span class="layer-name">company</span></header>
<h1>Levkin Inc.</h1>
<p class="tagline">LK-SPEC-1.0 · Software development · Canada</p>
<p class="layer-copy">Builds and maintains production software — custom apps, automation, practice systems. Scoped, documented, handoff-ready.</p>
<p class="avail">ACTIVE · Taking new engagements · remote NA &amp; EU</p>
</div>
</section>
<section class="layer layer-1" data-layer="1">
<div class="layer-inner">
<header class="layer-head"><button type="button" class="layer-id" data-goto="1">L1</button><span class="layer-name">scope</span></header>
<h2>1. Scope</h2>
<p class="layer-copy">Boutique practice — clear start, deliverable, and handoff. Discovery → fixed proposal → delivery → optional support.</p>
</div>
</section>
<section class="layer layer-2" data-layer="2">
<div class="layer-inner">
<header class="layer-head"><button type="button" class="layer-id" data-goto="2">L2</button><span class="layer-name">application</span></header>
<h2>2.1 Custom software</h2>
<p class="layer-copy">Web apps, APIs, internal tools — chosen to fit the problem, not a preset stack.</p>
</div>
</section>
<section class="layer layer-3" data-layer="3">
<div class="layer-inner">
<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>2.2 Automation</h2>
<p class="layer-copy">Background workflows — reporting, notifications, data sync, ops off your plate.</p>
</div>
</section>
<section class="layer layer-4" data-layer="4">
<div class="layer-inner">
<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>2.3 CaseWare</h2>
<p class="layer-copy">Templates, releases, practice customization — small fixes to full pipeline overhauls.</p>
</div>
</section>
<section class="layer layer-5" data-layer="5">
<div class="layer-inner">
<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>2.4 Quality &amp; testing</h2>
<p class="layer-copy">Test strategy, automation, release confidence — experienced QA lead without full-time hire.</p>
</div>
</section>
<section class="layer layer-6" data-layer="6">
<div class="layer-inner">
<header class="layer-head"><button type="button" class="layer-id" data-goto="6">L6</button><span class="layer-name">contact</span></header>
<h2>Terms &amp; contact</h2>
<p class="layer-copy">Fixed scope · production-ready · handoff-friendly · right-sized.</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:ilia@levkine.ca?subject=Project%20enquiry">ilia@levkine.ca</a>
</div>
</div>
</section>
<div class="stack-stop" aria-hidden="true"></div>
</main>
<footer class="site-foot"><a href="https://git.levkin.ca">git.levkin.ca</a><span>levkin.ca</span></footer>
<div class="stack-ruler" aria-label="Jump to layer">
<button type="button" data-goto="0" data-layer="0">L0</button>
<button type="button" data-goto="1" data-layer="1">L1</button>
<button type="button" data-goto="2" data-layer="2">L2</button>
<button type="button" data-goto="3" data-layer="3">L3</button>
<button type="button" data-goto="4" data-layer="4">L4</button>
<button type="button" data-goto="5" data-layer="5">L5</button>
<button type="button" data-goto="6" data-layer="6">L6</button>
</div>
<script type="module">
import { initStackScroll } from '../shared/stack-scroll.js';
initStackScroll();
</script>
</body>
</html>

View File

@ -1,119 +0,0 @@
@import '../shared/stack-vars.css';
@import '../shared/stack-layout.css';
:root {
--mono: 'IBM Plex Mono', ui-monospace, monospace;
--sans: 'Instrument Sans', system-ui, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--sans);
background: #0e0e10;
color: #e8e6e3;
line-height: 1.5;
}
.nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; gap: 1rem;
padding: 0.55rem 1rem; font-family: var(--mono); font-size: 0.62rem;
background: rgba(14, 14, 16, 0.92); backdrop-filter: blur(8px);
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.nav a { color: #6b6966; text-decoration: none; }
.nav a:hover { color: #c4a574; }
.variants { margin-left: auto; color: #4a4844; }
.depth { color: #c4a574; font-weight: 600; }
.stack-mount {
padding: var(--stack-nav) 1rem 0;
max-width: 560px;
margin: 0 auto;
}
/* Compact sticky card — click brings to front */
.layer:not(.layer-6) {
margin-bottom: var(--stack-scroll-slot);
}
.layer {
position: sticky;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.08);
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-1 { top: calc(var(--stack-stick) + var(--stack-step)); z-index: 2; background: #24242c; }
.layer-2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); z-index: 3; background: #2c2c36; }
.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-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: 0; }
.layer-inner { padding: 0.95rem 1.15rem 1.05rem; }
.layer-head {
display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.65rem;
margin-bottom: 0.65rem; padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(255,255,255,0.06);
font-family: var(--mono); font-size: 0.6rem;
}
.layer-id {
color: #c4a574; font-weight: 600; background: none; border: none;
font-family: var(--mono); font-size: 0.6rem; cursor: pointer; padding: 0;
}
.layer-id:hover { text-decoration: underline; }
.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 h1 { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.03em; }
.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; }
.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;
background: rgba(196,165,116,0.1); color: #c4a574; border: 1px solid rgba(196,165,116,0.2); border-radius: 3px;
}
.avail { font-family: var(--mono); font-size: 0.62rem; color: #7eb87a; }
.contact-row { display: flex; flex-wrap: wrap; gap: 0.45rem; margin: 0.65rem 0 0.4rem; }
.btn {
font-family: var(--mono); font-size: 0.68rem; padding: 0.45rem 0.75rem;
background: #c4a574; color: #0e0e10; text-decoration: none; font-weight: 600; border-radius: 4px;
}
.btn-ghost { background: transparent; color: #c4a574; border: 1px solid rgba(196,165,116,0.35); }
.guarantees { font-family: var(--mono); font-size: 0.58rem; color: #5a5854; }
.site-foot {
display: flex; justify-content: space-between;
max-width: 560px; margin: 0 auto;
padding: 0 1.5rem 2.5rem; font-family: var(--mono); font-size: 0.6rem; color: #4a4844;
}
.site-foot a { color: #4a4844; text-decoration: none; }
.stack-ruler {
position: fixed; right: max(0.5rem, calc(50% - 300px)); top: 50%; transform: translateY(-50%);
z-index: 50; display: flex; flex-direction: column; gap: 0.12rem;
font-family: var(--mono); font-size: 0.48rem; color: #2a2a30;
}
.stack-ruler button {
background: none; border: none; color: inherit; font: inherit; cursor: pointer; padding: 0.1rem 0; text-align: right;
}
.stack-ruler button:hover,
.stack-ruler button.active { color: #c4a574; }
@media (max-width: 700px) {
:root { --stack-step: 1.5rem; }
.variants { display: none; }
}

View File

@ -1,107 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Levkin — Software Development</title>
<meta name="description" content="Levkin — trusted software development for enterprise teams." />
<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 />
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,400&family=Source+Sans+3:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./vault.css" />
</head>
<body>
<div class="frame">
<nav class="nav">
<a href="/" class="nav-back">All options</a>
<span class="seal">Levkin</span>
</nav>
<header class="hero">
<p class="charter">Chartered software practice · Canada</p>
<h1>Built to hold.<br /><em>Built to hand off.</em></h1>
<p class="lead">Levkin develops production systems, automation, and enterprise software for organizations that cannot afford failure in the field. Boutique practice — fixed scope, documented handoff, optional stewardship.</p>
<p class="avail">Currently accepting new engagements · Remote (North America &amp; Europe)</p>
</header>
<section class="proof">
<div class="proof-grid">
<div class="proof-item">
<span class="proof-val">15+</span>
<span class="proof-label">Years in enterprise software</span>
</div>
<div class="proof-item">
<span class="proof-val">8h → &lt;2m</span>
<span class="proof-label">Release cycle reduction (CaseWare)</span>
</div>
<div class="proof-item">
<span class="proof-val">24/7</span>
<span class="proof-label">Automation that runs unattended</span>
</div>
</div>
<p class="clients">Delivered for teams at CaseWare International, MNP, JazzIt, and private accounting firms.</p>
</section>
<section class="holdings">
<h2 class="section-label">Holdings</h2>
<div class="holding-grid">
<article class="holding">
<h3>Custom software</h3>
<p>TypeScript, Python, C#/.NET — web apps, APIs, internal tools. PostgreSQL, SQL Server.</p>
</article>
<article class="holding">
<h3>Automation</h3>
<p>n8n, Zapier, Make, GitHub Actions, webhooks, LLM workflows. <a href="https://auto.levkin.ca">auto.levkin.ca</a></p>
</article>
<article class="holding">
<h3>CaseWare practice</h3>
<p>Features, CaseView templates, release pipelines — C#, .NET, Jenkins, Azure DevOps. <a href="https://caseware.levkin.ca">caseware.levkin.ca</a></p>
</article>
<article class="holding">
<h3>Internal operations</h3>
<p>Job Ops orchestrator — per-user hiring workflows (authenticated). <a href="https://jobs.levkin.ca">jobs.levkin.ca</a></p>
</article>
<article class="holding">
<h3>Quality engineering</h3>
<p>Senior SDET — Playwright-style portfolio, CI/CD, trace-driven QA. <a href="https://iliadobkin.com">iliadobkin.com</a></p>
</article>
</div>
</section>
<section class="covenant">
<h2 class="section-label">Covenant</h2>
<ul>
<li><span class="mark">§</span> Reliability is documented, not assumed</li>
<li><span class="mark">§</span> Every deliverable includes a handoff path</li>
<li><span class="mark">§</span> Tests precede production data</li>
<li><span class="mark">§</span> Scope changes require explicit amendment</li>
</ul>
</section>
<section class="process">
<h2 class="section-label">Engagement</h2>
<ol>
<li><strong>Discovery</strong> — 15-minute call; honest read on whether software fits</li>
<li><strong>Proposal</strong> — Fixed scope, tools, timeline, and cost — no surprises</li>
<li><strong>Delivery</strong> — Survives bad data, retries, and Monday morning</li>
<li><strong>Transition</strong> — Handoff with runbooks, or ongoing monitoring</li>
</ol>
</section>
<section class="contact">
<div class="contact-panel">
<p class="contact-title">Open an enquiry</p>
<a href="https://cal.levkin.ca/ilia/consult" class="contact-email">Book 15 min consultation</a>
<a href="mailto:hello@levkine.ca?subject=Vault%20enquiry" class="contact-email contact-email-secondary">hello@levkine.ca</a>
<p class="contact-note">Remote · North America &amp; Europe</p>
</div>
</section>
<footer>
<span>levkin.ca</span>
<a href="https://git.levkin.ca">Source repository</a>
</footer>
</div>
</body>
</html>

View File

@ -1,293 +0,0 @@
:root {
--forest: #0c1410;
--forest-mid: #142820;
--brass: #c9b896;
--brass-dim: rgba(201, 184, 150, 0.5);
--cream: #f0ebe3;
--muted: #7a8f82;
--serif: 'Cormorant Garamond', Georgia, serif;
--sans: 'Source Sans 3', system-ui, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--sans);
background: var(--forest);
color: var(--cream);
min-height: 100vh;
line-height: 1.6;
}
.frame {
max-width: 680px;
margin: 0 auto;
padding: 0 2rem 4rem;
border-left: 1px solid var(--brass-dim);
border-right: 1px solid var(--brass-dim);
min-height: 100vh;
background: linear-gradient(180deg, var(--forest-mid) 0%, var(--forest) 30%);
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 0;
border-bottom: 1px solid var(--brass-dim);
font-size: 0.8rem;
}
.nav-back {
color: var(--muted);
text-decoration: none;
letter-spacing: 0.04em;
}
.nav-back:hover { color: var(--brass); }
.seal {
font-family: var(--serif);
font-size: 1.1rem;
font-weight: 600;
color: var(--brass);
letter-spacing: 0.12em;
}
.hero {
padding: 3.5rem 0 3rem;
border-bottom: 1px solid var(--brass-dim);
}
.charter {
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 1.25rem;
}
.hero h1 {
font-family: var(--serif);
font-size: clamp(2rem, 6vw, 2.75rem);
font-weight: 500;
line-height: 1.2;
margin-bottom: 1.25rem;
color: var(--cream);
}
.hero h1 em {
font-style: italic;
color: var(--brass);
}
.lead {
font-size: 1.05rem;
color: var(--muted);
max-width: 44ch;
line-height: 1.65;
}
.avail {
margin-top: 1rem;
font-size: 0.8rem;
letter-spacing: 0.08em;
color: var(--brass);
}
.proof {
padding-bottom: 1rem;
border-bottom: 1px solid var(--brass-dim);
margin-bottom: 0.5rem;
}
.proof-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1.25rem;
}
@media (max-width: 500px) {
.proof-grid { grid-template-columns: 1fr; }
}
.proof-item {
text-align: center;
padding: 1rem 0.5rem;
border: 1px solid var(--brass-dim);
}
.proof-val {
display: block;
font-family: var(--serif);
font-size: 1.5rem;
color: var(--brass);
margin-bottom: 0.25rem;
}
.proof-label {
font-size: 0.72rem;
color: var(--muted);
line-height: 1.4;
}
.clients {
font-size: 0.88rem;
color: var(--muted);
font-style: italic;
text-align: center;
}
.section-label {
font-family: var(--serif);
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.25em;
text-transform: uppercase;
color: var(--brass);
margin-bottom: 1.25rem;
padding-top: 2.5rem;
}
.holdings { padding-bottom: 1rem; }
.holding-grid {
display: grid;
gap: 1px;
background: var(--brass-dim);
}
.holding {
background: var(--forest);
padding: 1.35rem 1.25rem;
}
.holding h3 {
font-family: var(--serif);
font-size: 1.2rem;
font-weight: 600;
color: var(--brass);
margin-bottom: 0.35rem;
}
.holding p {
font-size: 0.9rem;
color: var(--muted);
}
.holding a {
color: var(--brass);
text-decoration: none;
}
.holding a:hover { text-decoration: underline; }
.covenant ul {
list-style: none;
}
.covenant li {
padding: 0.6rem 0;
border-bottom: 1px solid rgba(201, 184, 150, 0.12);
font-size: 0.95rem;
color: var(--muted);
display: flex;
gap: 0.75rem;
}
.mark {
color: var(--brass);
font-family: var(--serif);
}
.process ol {
list-style: none;
counter-reset: step;
}
.process li {
counter-increment: step;
padding: 0.75rem 0;
border-bottom: 1px solid rgba(201, 184, 150, 0.12);
font-size: 0.9rem;
color: var(--muted);
padding-left: 2rem;
position: relative;
}
.process li::before {
content: counter(step, decimal-leading-zero);
position: absolute;
left: 0;
font-family: var(--serif);
color: var(--brass);
font-size: 0.85rem;
}
.process strong {
color: var(--cream);
font-weight: 500;
}
.contact {
padding: 3rem 0 2rem;
}
.contact-panel {
border: 1px solid var(--brass);
padding: 2rem;
text-align: center;
background: rgba(201, 184, 150, 0.04);
}
.contact-title {
font-family: var(--serif);
font-size: 0.9rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--brass);
margin-bottom: 0.75rem;
}
.contact-email {
font-family: var(--serif);
font-size: 1.5rem;
color: var(--cream);
text-decoration: none;
display: inline-block;
margin-bottom: 0.5rem;
}
.contact-email:hover { color: var(--brass); }
.contact-email-secondary {
display: block;
font-size: 1rem;
margin-top: 0.75rem;
color: var(--muted);
}
.contact-email-secondary:hover { color: var(--brass); }
.contact-note {
font-size: 0.8rem;
color: var(--muted);
}
footer {
display: flex;
justify-content: space-between;
padding-top: 2rem;
border-top: 1px solid var(--brass-dim);
font-size: 0.75rem;
color: var(--muted);
letter-spacing: 0.04em;
}
footer a {
color: var(--muted);
text-decoration: none;
}
footer a:hover { color: var(--brass); }

View File

@ -7,7 +7,6 @@ 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'),
stack: resolve(__dirname, 'stack/index.html'),
stackFolder: resolve(__dirname, 'stack-folder/index.html'), stackFolder: resolve(__dirname, 'stack-folder/index.html'),
}, },
}, },