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:
parent
21c75cdcba
commit
85229b09c8
57
README.md
57
README.md
@ -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 L0–L7, sticky tab stack, site previews |
|
||||||
| **Folder** | `/stack-folder/` | Manila folders, staggered tabs (L0–L7), 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 (L0–L7) 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
|
||||||
- **L3–L5** — 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)
|
||||||
|
|||||||
43
index.html
43
index.html
@ -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 L0–L6 scroll stack — click a layer to bring it to the front.</p>
|
<p class="lead">Spec for the company story. Folder for the L0–L7 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 L0–L6 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>
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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 & 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>
|
|
||||||
240
relay/relay.css
240
relay/relay.css
@ -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); }
|
|
||||||
@ -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();
|
|
||||||
@ -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'],
|
||||||
|
|||||||
@ -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();
|
|
||||||
@ -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();
|
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
@ -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));
|
|
||||||
@ -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 & 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>
|
|
||||||
240
slab/slab.css
240
slab/slab.css
@ -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 |
@ -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>
|
|
||||||
@ -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; }
|
|
||||||
@ -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>
|
|
||||||
@ -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; }
|
|
||||||
102
stack/index.html
102
stack/index.html
@ -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 & 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 & 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 & 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>
|
|
||||||
119
stack/stack.css
119
stack/stack.css
@ -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; }
|
|
||||||
}
|
|
||||||
107
vault/index.html
107
vault/index.html
@ -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 & 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 → <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 & Europe</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<span>levkin.ca</span>
|
|
||||||
<a href="https://git.levkin.ca">Source repository</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
293
vault/vault.css
293
vault/vault.css
@ -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); }
|
|
||||||
@ -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'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user