diff --git a/.gitignore b/.gitignore
index 3bdd52e..5587ad2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
node_modules/
dist/
.DS_Store
+.scroll-debug/
diff --git a/README.md b/README.md
index 1fe564f..58b72d4 100644
--- a/README.md
+++ b/README.md
@@ -2,25 +2,13 @@
Design concepts for the Levkin software development company homepage.
-### Brand directions
-
| Option | Path | Vibe |
|--------|------|------|
| **Spec** | `/spec/` | RFC documentation, endpoints, iliadobkin.com |
-| **Slab** | `/slab/` | Brutalist poster |
-| **Relay** | `/relay/` | Telegraph, decoded messages |
-| **Vault** | `/vault/` | Institutional trust |
+| **Cards** | `/stack/` | Dark overlapping sticky cards — click a layer to bring it forward |
+| **Folder** | `/stack-folder/` | Manila folders, staggered tabs (L0–L7), site previews |
-### Stack variants (L0–L6, scroll stops at interface)
-
-| Variant | Path | Metaphor |
-|---------|------|----------|
-| **Cards** | `/stack/` | Dark overlapping sticky cards |
-| **Folder** | `/stack-folder/` | Manila folders, top tabs stay visible |
-| **Trace** | `/stack-trace/` | Call-stack / devtools frames |
-| **Rack** | `/stack-rack/` | Server rack 1U units with status LEDs |
-
-Open `/` to compare all.
+Open `/` to compare all three.
## Develop
@@ -30,6 +18,15 @@ npm install
npm run dev
```
+Vite serves the multi-page app (default `http://localhost:5173`). If that port is busy, pass another: `npx vite --port 5175`.
+
+| Page | URL |
+|------|-----|
+| Compare | `/` |
+| Spec | `/spec/` |
+| Cards | `/stack/` |
+| Folders | `/stack-folder/` |
+
## Build
```bash
@@ -37,6 +34,42 @@ npm run build
# output in dist/
```
+## Folder site (`/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`).
+
+- **L0** — Company + Cal embed
+- **L1** — Scope + spec preview
+- **L2** — Services
+- **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
+
+Each linked folder shows a screenshot; refresh captures with dev server running:
+
+```bash
+npm run dev # separate terminal
+npm run capture-previews
+```
+
+Writes PNGs to `stack-folder/previews/`.
+
+### Tests (Playwright)
+
+Requires dev server on the URL you pass:
+
+```bash
+npm run dev
+STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder
+STACK_URL=http://localhost:5173/stack-folder/ npm run test:stack-scroll
+```
+
+`test:folder` checks tab alignment at max scroll and L7 jump. `test:stack-scroll` covers scroll/blur behavior on the card stack.
+
## Related sites
- [auto.levkin.ca](https://auto.levkin.ca) — automation
diff --git a/index.html b/index.html
index 79c67a4..0ccfaec 100644
--- a/index.html
+++ b/index.html
@@ -24,7 +24,7 @@
min-height: 100vh;
line-height: 1.5;
}
- .wrap { max-width: 1100px; margin: 0 auto; padding: 4rem 1.5rem 6rem; }
+ .wrap { max-width: 900px; margin: 0 auto; padding: 4rem 1.5rem 6rem; }
header { margin-bottom: 3rem; }
.eyebrow {
font-family: 'DM Mono', monospace;
@@ -44,15 +44,7 @@
.grid {
display: grid;
gap: 1.25rem;
- }
- @media (min-width: 640px) {
- .grid { grid-template-columns: repeat(2, 1fr); }
- }
- @media (min-width: 900px) {
- .grid { grid-template-columns: repeat(3, 1fr); }
- }
- @media (min-width: 1200px) {
- .grid { grid-template-columns: repeat(5, 1fr); }
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
a.card {
display: block;
@@ -68,12 +60,10 @@
transform: translateY(-3px);
}
.preview {
- height: 130px;
+ height: 110px;
display: flex;
align-items: center;
justify-content: center;
- position: relative;
- overflow: hidden;
font-size: 0.7rem;
}
.preview--spec {
@@ -82,35 +72,6 @@
font-family: 'DM Mono', monospace;
letter-spacing: 0.06em;
}
- .preview--slab {
- background: #f5f5f0;
- color: #0a0a0a;
- font-weight: 800;
- font-size: 2rem;
- letter-spacing: -0.06em;
- font-family: system-ui, sans-serif;
- }
- .preview--relay {
- background: #1a1814;
- color: #d4a574;
- font-family: 'DM Mono', monospace;
- letter-spacing: 0.2em;
- }
- .preview--relay::after {
- content: '· · · ─ ─ ·';
- position: absolute;
- bottom: 1rem;
- font-size: 0.55rem;
- opacity: 0.5;
- }
- .preview--vault {
- background: linear-gradient(160deg, #0c1410 0%, #1a2820 100%);
- color: #c9b896;
- font-family: 'DM Sans', sans-serif;
- font-size: 0.65rem;
- letter-spacing: 0.25em;
- text-transform: uppercase;
- }
.preview--stack {
background: #0e0e10;
flex-direction: column;
@@ -119,17 +80,21 @@
}
.preview--stack .card-layer {
width: 70%;
- height: 14px;
+ height: 12px;
border-radius: 3px;
border: 1px solid rgba(255,255,255,0.1);
background: linear-gradient(90deg, #2a2a32, #3a3a46);
- box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
.preview--stack .card-layer:nth-child(1) { width: 55%; opacity: 0.5; }
- .preview--stack .card-layer:nth-child(2) { width: 62%; opacity: 0.65; }
- .preview--stack .card-layer:nth-child(3) { width: 68%; opacity: 0.8; }
- .preview--stack .card-layer:nth-child(4) { width: 75%; }
- .preview--stack .card-layer:nth-child(5) { width: 82%; background: #4a4a58; }
+ .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 {
+ background: #e8e2d4;
+ color: #2a2824;
+ font-family: 'DM Mono', monospace;
+ font-size: 0.65rem;
+ border-left: 12px solid #c9a86c;
+ }
.card-body { padding: 1.15rem 1.25rem 1.35rem; }
.card-body h2 { font-size: 1.05rem; font-weight: 600; margin-bottom: 0.3rem; }
.card-body p { font-size: 0.82rem; color: var(--muted); line-height: 1.4; }
@@ -141,13 +106,6 @@
color: var(--accent);
letter-spacing: 0.06em;
}
- .kept {
- display: inline-block;
- font-family: 'DM Mono', monospace;
- font-size: 0.6rem;
- color: var(--muted);
- margin-left: 0.35rem;
- }
footer {
margin-top: 3.5rem;
padding-top: 2rem;
@@ -157,120 +115,43 @@
}
footer a { color: var(--accent); text-decoration: none; }
footer code { font-family: 'DM Mono', monospace; font-size: 0.75rem; }
- .section-label {
- font-family: 'DM Mono', monospace;
- font-size: 0.68rem;
- letter-spacing: 0.14em;
- text-transform: uppercase;
- color: var(--muted);
- margin: 2rem 0 1rem;
- }
- .grid-stacks { margin-bottom: 0; }
- .preview--folder {
- background: #e8e2d4;
- color: #2a2824;
- font-family: 'DM Mono', monospace;
- font-size: 0.65rem;
- border-left: 12px solid #c9a86c;
- }
- .preview--rack {
- background: #12141a;
- color: #4ade80;
- font-family: 'DM Mono', monospace;
- font-size: 0.55rem;
- letter-spacing: 0.15em;
- border: 2px solid #2a3040;
- }
- .preview--trace {
- background: #0d0d0f;
- color: #6b9b6b;
- font-family: 'DM Mono', monospace;
- font-size: 0.6rem;
- }
- levkin.ca · round 3
- Eight directions.
- Five brand concepts + four stack variants (L0–L6, stops on time). Spec updated with iliadobkin.com.
+ levkin.ca
+ Three directions.
+ Spec for the company story. Cards and Folder for the L0–L6 scroll stack — click a layer to bring it to the front.
-
Brand
-
-
Stack variants (scroll test)
-
diff --git a/package.json b/package.json
index 08c58e8..7947630 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,10 @@
"scripts": {
"dev": "vite",
"build": "vite build",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "capture-previews": "node scripts/capture-previews.mjs",
+ "test:folder": "node scripts/test-stack-folder.mjs",
+ "test:stack-scroll": "node scripts/test-stack-scroll.mjs"
},
"devDependencies": {
"playwright": "^1.60.0",
diff --git a/scripts/capture-previews.mjs b/scripts/capture-previews.mjs
new file mode 100644
index 0000000..c50cbe5
--- /dev/null
+++ b/scripts/capture-previews.mjs
@@ -0,0 +1,82 @@
+#!/usr/bin/env node
+/** Refresh stack-folder/previews/*.png — run dev server first for local shots */
+import { chromium } from 'playwright';
+import { mkdirSync } from 'fs';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+
+const root = join(dirname(fileURLToPath(import.meta.url)), '..');
+const out = join(root, 'stack-folder/previews');
+const base = process.env.PREVIEW_BASE || 'http://localhost:5177';
+
+mkdirSync(out, { recursive: true });
+
+const shots = [
+ ['spec', `${base}/spec/`],
+ ['stack', `${base}/stack/`],
+ ['auto', 'https://auto.levkin.ca'],
+ ['caseware', 'https://caseware.levkin.ca'],
+ ['iliadobkin', 'https://iliadobkin.com'],
+ ['git-repos', 'https://git.levkin.ca/explore/repos'],
+ ['cal', 'https://cal.levkin.ca/ilia/consult'],
+];
+
+async function setCalTheme(page, mode) {
+ await page.evaluate((want) => {
+ const root = document.documentElement;
+ const btn = [...document.querySelectorAll('button, [role="button"], label, a')].find(
+ (el) => new RegExp(want, 'i').test(el.textContent || el.getAttribute('aria-label') || ''),
+ );
+ if (btn) btn.click();
+ if (want === 'dark') {
+ root.dataset.theme = 'dark';
+ root.classList.add('dark');
+ } else {
+ root.dataset.theme = 'light';
+ root.classList.remove('dark');
+ }
+ }, mode);
+}
+
+async function captureCal(page, outDir, theme) {
+ const url = 'https://cal.levkin.ca/ilia/consult';
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 25000 });
+ await page.waitForTimeout(1200);
+ await setCalTheme(page, theme);
+ await page.waitForTimeout(1500);
+
+ await page.evaluate(() => {
+ window.scrollTo(0, 120);
+ });
+ await page.waitForTimeout(600);
+
+ const file = theme === 'light' ? 'cal-light.png' : 'cal-dark.png';
+ await page.screenshot({
+ path: join(outDir, file),
+ clip: { x: 0, y: 0, width: 1280, height: 680 },
+ });
+ console.log(`✓ ${file}`);
+}
+
+const browser = await chromium.launch({ headless: true });
+const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
+
+for (const [name, url] of shots) {
+ try {
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
+ await page.waitForTimeout(1500);
+ await page.screenshot({ path: join(out, `${name}.png`) });
+ console.log(`✓ ${name}.png`);
+ } catch (err) {
+ console.warn(`✗ ${name}: ${err.message}`);
+ }
+}
+
+try {
+ await captureCal(page, out, 'dark');
+ await captureCal(page, out, 'light');
+} catch (err) {
+ console.warn(`✗ cal captures: ${err.message}`);
+}
+
+await browser.close();
diff --git a/scripts/test-stack-folder.mjs b/scripts/test-stack-folder.mjs
new file mode 100644
index 0000000..180dc83
--- /dev/null
+++ b/scripts/test-stack-folder.mjs
@@ -0,0 +1,64 @@
+/**
+ * Tab rail alignment tests for /stack-folder/.
+ * Run: STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder
+ */
+import { chromium } from 'playwright';
+
+const URL = process.env.STACK_URL || 'http://localhost:5173/stack-folder/';
+const VERBOSE = process.env.VERBOSE === '1';
+
+const browser = await chromium.launch();
+const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
+await page.goto(URL, { waitUntil: 'networkidle' });
+
+async function tabTops() {
+ return page.evaluate(() => {
+ const tabs = [...document.querySelectorAll('.mount .tab')];
+ const stick =
+ parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--stack-stick')) * 16;
+ return {
+ scrollY: window.scrollY,
+ isFolded: document.querySelector('.mount')?.classList.contains('is-folded'),
+ stick,
+ tabs: tabs.map((t) => ({
+ code: t.querySelector('.tab-code')?.textContent,
+ top: Math.round(t.getBoundingClientRect().top),
+ left: Math.round(t.getBoundingClientRect().left),
+ })),
+ };
+ });
+}
+
+if (VERBOSE) console.log('initial', await tabTops());
+
+await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
+await page.waitForTimeout(600);
+if (VERBOSE) console.log('max scroll', await tabTops());
+
+await page.click('[data-goto="7"]');
+await page.waitForTimeout(800);
+if (VERBOSE) console.log('goto L7', await tabTops());
+
+const fail = await page.evaluate(() => {
+ const tabs = [...document.querySelectorAll('.mount .tab')];
+ const tops = tabs.map((t) => t.getBoundingClientRect().top);
+ const min = Math.min(...tops);
+ const max = Math.max(...tops);
+ const l7 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L7');
+ const l0 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L0');
+ const issues = [];
+ if (max - min > 30) issues.push(`tab row spread ${Math.round(max - min)}px (want ≤30)`);
+ if (l7 && l0 && Math.abs(l7.getBoundingClientRect().top - l0.getBoundingClientRect().top) > 30)
+ issues.push('L7 not aligned with L0');
+ if (!document.querySelector('.mount')?.classList.contains('is-folded'))
+ issues.push('mount not folded at max scroll');
+ return issues;
+});
+
+await browser.close();
+
+if (fail.length) {
+ console.error('FAIL:', fail.join('; '));
+ process.exit(1);
+}
+console.log('PASS: stack-folder tab rail');
diff --git a/scripts/test-stack-scroll.mjs b/scripts/test-stack-scroll.mjs
new file mode 100644
index 0000000..0753692
--- /dev/null
+++ b/scripts/test-stack-scroll.mjs
@@ -0,0 +1,118 @@
+/**
+ * Automated scroll/blur tests for stack-folder.
+ * Run: node scripts/test-stack-scroll.mjs
+ */
+import { chromium } from 'playwright';
+
+const URL = process.env.STACK_URL || 'http://localhost:5173/stack-folder/';
+const VIEWPORT = { width: 1280, height: 800 };
+
+function fail(msg) {
+ console.error('FAIL:', msg);
+ process.exitCode = 1;
+}
+
+function pass(msg) {
+ console.log('PASS:', msg);
+}
+
+async function readState(page) {
+ return page.evaluate(() => {
+ const stick =
+ parseFloat(getComputedStyle(document.documentElement).fontSize) * 3;
+ const l7Tab = document.querySelector('.f7 .tab');
+ const l0 = document.querySelector('.f0');
+ const l0Body = document.querySelector('.f0 .body');
+ const folderBlur = l0?.style.getPropertyValue('--stack-blur') || '';
+ const filter = l0Body ? getComputedStyle(l0Body).filter : '';
+ const blurMatch = filter.match(/blur\(([\d.]+)px\)/);
+ const blurPx = blurMatch ? parseFloat(blurMatch[1]) : 0;
+ return {
+ scrollY: window.scrollY,
+ docMax: document.documentElement.scrollHeight - innerHeight,
+ l7TabTop: l7Tab?.getBoundingClientRect().top ?? null,
+ stick,
+ l0Covered: l0?.classList.contains('is-covered'),
+ l0BlurPx: blurPx,
+ folderBlur,
+ l0Filter: filter,
+ runway: getComputedStyle(document.querySelector('.mount')).getPropertyValue(
+ '--stack-runway',
+ ),
+ };
+ });
+}
+
+async function main() {
+ const browser = await chromium.launch();
+ const page = await browser.newPage({ viewport: VIEWPORT });
+ await page.goto(URL, { waitUntil: 'networkidle' });
+ await page.waitForTimeout(400);
+
+ const top = await readState(page);
+ if (top.l0Covered || top.l0BlurPx > 0.1) {
+ fail(`L0 blurred at top (covered=${top.l0Covered}, blur=${top.l0BlurPx})`);
+ } else {
+ pass('L0 clear at scroll top');
+ }
+
+ await page.evaluate(() => window.scrollTo(0, 999999));
+ await page.waitForTimeout(350);
+ const end = await readState(page);
+
+ if (end.l7TabTop === null) fail('L7 tab missing');
+ else if (Math.abs(end.l7TabTop - end.stick) > 8) {
+ fail(`L7 tab not on stick: top=${end.l7TabTop} stick=${end.stick} scrollY=${end.scrollY}`);
+ } else {
+ pass(`L7 on stick at max scroll (y=${end.scrollY}, tabTop=${end.l7TabTop.toFixed(1)})`);
+ }
+
+ if (end.l0BlurPx < 2) {
+ fail(`L0 not blurred when stacked (blur=${end.l0BlurPx})`);
+ } else {
+ pass(`L0 blurred when stacked (blur=${end.l0BlurPx}px)`);
+ }
+
+ const midY = Math.floor(end.scrollY * 0.45);
+ await page.evaluate((y) => window.scrollTo(0, y), midY);
+ await page.waitForTimeout(200);
+ const mid = await readState(page);
+ if (mid.l0BlurPx > 2) {
+ fail(`L0 still heavily blurred mid-scroll out (blur=${mid.l0BlurPx} at y=${mid.scrollY})`);
+ } else {
+ pass(`L0 unfades when scrolling out (blur=${mid.l0BlurPx}px at y=${mid.scrollY})`);
+ }
+
+ await page.evaluate(() => window.scrollTo(0, 0));
+ await page.waitForTimeout(350);
+ const back = await readState(page);
+ if (back.l0Covered || back.l0BlurPx > 0.1) {
+ fail(`L0 still blurred after scroll to top (covered=${back.l0Covered}, blur=${back.l0BlurPx})`);
+ } else {
+ pass('L0 unfades after scroll back to top');
+ }
+
+ const maxY = end.scrollY;
+ await page.evaluate((y) => window.scrollTo(0, y), maxY);
+ await page.waitForTimeout(200);
+ await page.evaluate(() => window.scrollTo(0, 0));
+ await page.waitForTimeout(350);
+ const back2 = await readState(page);
+ if (back2.l0Covered || back2.l0BlurPx > 0.1) {
+ fail(`L0 stuck after down-up cycle (blur=${back2.l0BlurPx})`);
+ } else {
+ pass('L0 unfades after full down-up cycle');
+ }
+
+ await browser.close();
+ if (process.exitCode) {
+ console.log('\nTests failed.');
+ process.exit(1);
+ }
+ console.log('\nAll tests passed.');
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/shared/stack-layout.css b/shared/stack-layout.css
index e67d41f..4704e04 100644
--- a/shared/stack-layout.css
+++ b/shared/stack-layout.css
@@ -1,6 +1,6 @@
-/* Spacer after L6 so scroll can stop */
.stack-stop,
.stop {
- height: 1px;
- margin-bottom: 4rem;
+ height: 0;
+ margin: 0;
+ pointer-events: none;
}
diff --git a/shared/stack-scroll.js b/shared/stack-scroll.js
index 85dba7d..047a4f8 100644
--- a/shared/stack-scroll.js
+++ b/shared/stack-scroll.js
@@ -1,43 +1,329 @@
-/** Scroll depth + jump — panels are [data-layer] sticky cards */
+/** Scroll depth, jump, folder fold state */
export function initStackScroll(options = {}) {
const {
- sectionSelector = '.layer[data-layer], .folder[data-layer], .frame[data-layer], .unit[data-layer]',
+ sectionSelector = '.layer[data-layer], .folder[data-layer]',
depthEl = document.getElementById('depth'),
depthPrefix = 'L',
- tabSelector = '[data-goto], .jump',
+ tabSelector = '[data-goto], .jump, .layer-id',
+ mountSelector = '.mount',
+ foldTabs = false,
+ interactionMode = 'pin',
} = options;
const sections = document.querySelectorAll(sectionSelector);
if (!sections.length) return;
- const mid = () => window.innerHeight * 0.45;
+ const mount = document.querySelector(mountSelector);
+ const lastLayer = sections.length - 1;
+ const scrollPad = 56;
+ let frontLayer = null;
+ let layerScrollTops = [];
+ let maxScrollY = Infinity;
+ let measuringScroll = false;
+ const deckTabs = () =>
+ mount ? [...mount.querySelectorAll('.folder .tab')] : [];
- function updateDepth() {
- let active = 0;
- sections.forEach((el) => {
- const r = el.getBoundingClientRect();
- if (r.top <= mid() && r.bottom > mid()) active = Number(el.dataset.layer);
- });
- if (depthEl) depthEl.textContent = `${depthPrefix}${active}`;
+ function layerAnchor(el) {
+ return el.querySelector('.tab') || el.querySelector('.body') || el;
+ }
- document.querySelectorAll('.stack-ruler button, .stack-ruler [data-goto], .tab-rail button, .tab[data-goto]').forEach((tab) => {
- const n = tab.dataset.layer ?? tab.dataset.goto;
+ function tabRowMetrics() {
+ const tabs = deckTabs();
+ const stick = stackStickPx();
+ const tops = tabs.map((t) => t.getBoundingClientRect().top);
+ const spread = tops.length ? Math.max(...tops) - Math.min(...tops) : 0;
+ const onStick = tops.length
+ ? tops.every((top) => Math.abs(top - stick) <= 6)
+ : false;
+ return { tabs, stick, spread, onStick };
+ }
+
+ function stackStickPx() {
+ const root = document.documentElement;
+ const raw = getComputedStyle(root).getPropertyValue('--stack-stick').trim();
+ const n = parseFloat(raw);
+ if (!Number.isFinite(n)) return scrollPad;
+ if (raw.endsWith('rem')) {
+ return n * parseFloat(getComputedStyle(root).fontSize);
+ }
+ if (raw.endsWith('px')) return n;
+ return n;
+ }
+
+ function captureLayerAnchors(force = false) {
+ if (!force && window.scrollY > 8 && layerScrollTops.length) return;
+ const stick = stackStickPx();
+ layerScrollTops = [...sections].map((el) =>
+ Math.max(0, layerAnchor(el).offsetTop - stick),
+ );
+ }
+
+ function lastTabEl() {
+ return sections[lastLayer]?.querySelector('.tab') ?? null;
+ }
+
+ function measureDeskEnd() {
+ const tab = lastTabEl();
+ if (!tab) return 0;
+ return Math.max(0, tab.offsetTop - stackStickPx());
+ }
+
+ function ensureStackRunway() {
+ if (!mount) return;
+ measuringScroll = true;
+ try {
+ captureLayerAnchors(true);
+ mount.style.setProperty('--stack-runway', '0px');
+ void mount.offsetHeight;
+
+ const stick = stackStickPx();
+ const tab = lastTabEl();
+ if (!tab) return;
+
+ for (let i = 0; i < 12; i++) {
+ const docMax = Math.max(
+ 0,
+ document.documentElement.scrollHeight - window.innerHeight,
+ );
+ window.scrollTo({ top: docMax, behavior: 'auto' });
+ void mount.offsetHeight;
+ const shortfall = Math.ceil(tab.getBoundingClientRect().top - stick);
+ if (shortfall <= 1) break;
+
+ const cur =
+ parseFloat(getComputedStyle(mount).getPropertyValue('--stack-runway')) || 0;
+ mount.style.setProperty('--stack-runway', `${cur + shortfall + 2}px`);
+ void mount.offsetHeight;
+ captureLayerAnchors(true);
+ }
+ } finally {
+ measuringScroll = false;
+ window.scrollTo({ top: 0, behavior: 'auto' });
+ }
+ }
+
+ function tabsAlignedAtScroll() {
+ const { tabs, stick, spread } = tabRowMetrics();
+ if (!tabs.length) return false;
+ const tops = tabs.map((t) => t.getBoundingClientRect().top);
+ return (
+ spread <= 1 &&
+ tops.every((top) => Math.abs(top - stick) <= 2)
+ );
+ }
+
+ /** Furthest scroll Y where every tab is flush on the stick line */
+ function computeMaxScrollAligned() {
+ const docMax = Math.max(
+ 0,
+ document.documentElement.scrollHeight - window.innerHeight,
+ );
+ if (!deckTabs().length) return 0;
+
+ measuringScroll = true;
+ try {
+ for (let y = docMax; y >= 0; y -= 1) {
+ window.scrollTo({ top: y, behavior: 'auto' });
+ void mount?.offsetHeight;
+ if (tabsAlignedAtScroll()) return y;
+ }
+ return 0;
+ } finally {
+ measuringScroll = false;
+ window.scrollTo({ top: 0, behavior: 'auto' });
+ }
+ }
+
+ function captureMaxScroll() {
+ if (window.scrollY <= 8) {
+ captureLayerAnchors(true);
+ ensureStackRunway();
+ captureLayerAnchors(true);
+ }
+ maxScrollY = computeMaxScrollAligned();
+ }
+
+ function isFolded() {
+ if (!foldTabs || !mount) return false;
+ if (deckTabs().length < sections.length) return false;
+ return window.scrollY > 120 && tabsAlignedAtScroll();
+ }
+
+ function updateFolded() {
+ if (!mount) return;
+ mount.classList.toggle('is-folded', isFolded());
+ }
+
+ function clampScroll() {
+ if (measuringScroll) return;
+ if (window.scrollY > maxScrollY) {
+ window.scrollTo({ top: maxScrollY, behavior: 'auto' });
+ }
+ }
+
+ function layerTabTitle(layer) {
+ const section = sections[layer];
+ const tab = section?.querySelector('.tab');
+ if (!tab) return `${depthPrefix}${layer}`;
+ const code = tab.querySelector('.tab-code')?.textContent?.trim() ?? `${depthPrefix}${layer}`;
+ const label = tab.querySelector('.tab-label')?.textContent?.trim() ?? '';
+ return `${code}${label}`;
+ }
+
+ function setIndicators(active) {
+ if (depthEl) {
+ depthEl.textContent =
+ active === lastLayer && isFolded()
+ ? layerTabTitle(active)
+ : `${depthPrefix}${active}`;
+ }
+ document.querySelectorAll('.stack-ruler button, .stack-ruler [data-goto], .tab-rail button, .tab[data-goto], .layer-id').forEach((el) => {
+ const n = el.dataset.layer ?? el.dataset.goto;
if (n === undefined) return;
- tab.classList.toggle('active', Number(n) === active);
+ el.classList.toggle('active', Number(n) === active);
});
}
+ function setFront(layer) {
+ frontLayer = layer;
+ sections.forEach((el) => {
+ el.classList.toggle('is-front', layer !== null && Number(el.dataset.layer) === layer);
+ });
+ if (layer !== null) setIndicators(layer);
+ }
+
+ function clearFront() {
+ setFront(null);
+ }
+
+ function scrollToLayer(layer) {
+ if (isFolded() && layerScrollTops[layer] !== undefined) {
+ window.scrollTo({ top: layerScrollTops[layer], behavior: 'smooth' });
+ return;
+ }
+
+ const target =
+ document.getElementById(`layer-${layer}`) ||
+ document.querySelector(`${sectionSelector}[data-layer="${layer}"]`);
+ if (!target) return;
+ let y;
+ if (layer === lastLayer) {
+ captureMaxScroll();
+ y = maxScrollY;
+ } else {
+ const anchor = layerAnchor(target);
+ y = Math.min(
+ anchor.getBoundingClientRect().top + window.scrollY - scrollPad,
+ maxScrollY,
+ );
+ }
+ const behavior = layer === lastLayer ? 'auto' : 'smooth';
+ window.scrollTo({ top: Math.max(0, y), behavior });
+ if (layer === lastLayer) {
+ requestAnimationFrame(updateDepth);
+ }
+ }
+
+ function navigateToLayer(layer) {
+ clearFront();
+ scrollToLayer(layer);
+ requestAnimationFrame(() => {
+ updateFolded();
+ updateDepth();
+ });
+ }
+
+ function openLayer(layer) {
+ if (interactionMode === 'navigate' || isFolded()) {
+ navigateToLayer(layer);
+ return;
+ }
+
+ if (frontLayer === layer) {
+ clearFront();
+ updateDepth();
+ scrollToLayer(layer);
+ return;
+ }
+
+ if (frontLayer !== null) {
+ clearFront();
+ scrollToLayer(layer);
+ requestAnimationFrame(updateDepth);
+ return;
+ }
+
+ setFront(layer);
+ scrollToLayer(layer);
+ }
+
+ function currentActiveLayer() {
+ if (isFolded()) return lastLayer;
+ const y = window.scrollY;
+ let active = 0;
+ for (let i = sections.length - 1; i >= 0; i--) {
+ const top = layerScrollTops[i];
+ if (top !== undefined && y >= top - 4) {
+ active = i;
+ break;
+ }
+ }
+ return active;
+ }
+
+ function updateCovered() {
+ sections.forEach((el) => {
+ el.classList.remove('is-covered');
+ el.style.removeProperty('--stack-blur');
+ });
+ }
+
+ function updateDepth() {
+ if (measuringScroll) return;
+ clampScroll();
+ updateFolded();
+ if (frontLayer !== null) return;
+ const active = currentActiveLayer();
+ updateCovered();
+ setIndicators(active);
+ window.dispatchEvent(
+ new CustomEvent('stack-depth', { detail: { active } }),
+ );
+ }
+
+ window.addEventListener('stack-goto-layer', (e) => {
+ const layer = Number(e.detail?.layer);
+ if (Number.isFinite(layer)) openLayer(layer);
+ });
+
+ sections.forEach((panel) => {
+ panel.addEventListener('click', (e) => {
+ if (e.target.closest('a[href], button[data-goto], .layer-id, .rail-tab, .tab[data-goto], .site-preview, .cta-block, .cal-slot, [data-cal-embed]')) return;
+ openLayer(Number(panel.dataset.layer));
+ });
+ });
+
document.querySelectorAll(tabSelector).forEach((tab) => {
tab.addEventListener('click', (e) => {
e.preventDefault();
- const layer = tab.dataset.goto ?? tab.dataset.layer;
- const target = document.querySelector(`${sectionSelector}[data-layer="${layer}"]`);
- if (!target) return;
- const y = target.getBoundingClientRect().top + window.scrollY - 56;
- window.scrollTo({ top: Math.max(0, y), behavior: 'smooth' });
+ e.stopPropagation();
+ openLayer(Number(tab.dataset.goto ?? tab.dataset.layer));
});
});
- window.addEventListener('scroll', updateDepth, { passive: true });
+ captureMaxScroll();
updateDepth();
+
+ window.addEventListener('load', () => {
+ if (window.scrollY > 8) window.scrollTo(0, 0);
+ requestAnimationFrame(() => {
+ captureMaxScroll();
+ updateDepth();
+ });
+ });
+ window.addEventListener('resize', () => {
+ captureMaxScroll();
+ updateDepth();
+ }, { passive: true });
+ window.addEventListener('scroll', updateDepth, { passive: true });
}
diff --git a/shared/stack-vars.css b/shared/stack-vars.css
index ea6bb6e..017048b 100644
--- a/shared/stack-vars.css
+++ b/shared/stack-vars.css
@@ -1,7 +1,10 @@
-/* Original working sticky stack (cfca7aa) */
+/* Sticky stack — one viewport of scroll per layer until L6 is on top */
:root {
--stack-nav: 2.5rem;
- --stack-stick: 3.5rem;
- --stack-step: 2.75rem;
- --stack-card-min: 52vh;
+ --stack-stick: 3rem;
+ --stack-tab-h: 1.72rem;
+ --stack-step: 1.75rem;
+ --stack-gap: 0.35rem;
+ /* ~one folder body of scroll between layers (not a full viewport each) */
+ --stack-scroll-slot: 1.5rem;
}
diff --git a/stack-folder/folder-cal.js b/stack-folder/folder-cal.js
new file mode 100644
index 0000000..4b4389d
--- /dev/null
+++ b/stack-folder/folder-cal.js
@@ -0,0 +1,184 @@
+/**
+ * Cal.com inline embed — paste from Event type → consult → Embed → Inline.
+ * Uses embed.js (not a raw iframe). No “allowed domains” setting needed for this path.
+ * L0 = dark snippet, L7 = light snippet. Screenshot fallback if embed does not mount.
+ */
+const CAL_ORIGIN = 'https://cal.levkin.ca';
+const CAL_LINK = 'ilia/consult';
+const EMBED_SCRIPT = `${CAL_ORIGIN}/embed/embed.js`;
+
+const SLOTS = {
+ dark: {
+ ns: 'consult-l0',
+ inlineConfig: {
+ layout: 'week_view',
+ useSlotsViewOnSmallScreen: 'true',
+ theme: 'dark',
+ },
+ ui: { theme: 'dark', hideEventTypeDetails: true, layout: 'week_view' },
+ },
+ light: {
+ ns: 'consult-l7',
+ inlineConfig: {
+ layout: 'week_view',
+ useSlotsViewOnSmallScreen: 'true',
+ theme: 'light',
+ },
+ ui: { theme: 'light', hideEventTypeDetails: true, layout: 'week_view' },
+ },
+};
+
+let calApiReady;
+
+/** Loader from Cal embed UI */
+function bootCalLoader() {
+ if (window.Cal?.loaded) return;
+ (function (C, A, L) {
+ const p = (a, ar) => {
+ a.q.push(ar);
+ };
+ const d = C.document;
+ C.Cal =
+ C.Cal ||
+ function () {
+ const cal = C.Cal;
+ const ar = arguments;
+ if (!cal.loaded) {
+ cal.ns = {};
+ cal.q = cal.q || [];
+ d.head.appendChild(d.createElement('script')).src = A;
+ cal.loaded = true;
+ }
+ if (ar[0] === L) {
+ const api = function () {
+ p(api, arguments);
+ };
+ const namespace = ar[1];
+ api.q = api.q || [];
+ if (typeof namespace === 'string') {
+ cal.ns[namespace] = cal.ns[namespace] || api;
+ p(cal.ns[namespace], ar);
+ p(cal, ['initNamespace', namespace]);
+ } else p(cal, ar);
+ return;
+ }
+ p(cal, ar);
+ };
+ })(window, EMBED_SCRIPT, 'init');
+}
+
+function waitForEmbedScript() {
+ return new Promise((resolve) => {
+ const finish = () => resolve(window.Cal);
+ const attach = (s) => {
+ if (s.dataset.calReady) {
+ finish();
+ return;
+ }
+ s.addEventListener(
+ 'load',
+ () => {
+ s.dataset.calReady = '1';
+ finish();
+ },
+ { once: true },
+ );
+ s.addEventListener('error', () => resolve(null), { once: true });
+ };
+
+ const existing = document.querySelector(`script[src="${EMBED_SCRIPT}"]`);
+ if (existing) {
+ attach(existing);
+ return;
+ }
+
+ const mo = new MutationObserver(() => {
+ const s = document.querySelector(`script[src="${EMBED_SCRIPT}"]`);
+ if (!s) return;
+ mo.disconnect();
+ attach(s);
+ });
+ mo.observe(document.head, { childList: true });
+ window.setTimeout(() => {
+ mo.disconnect();
+ finish();
+ }, 12000);
+ });
+}
+
+function loadCalApi() {
+ if (!calApiReady) {
+ bootCalLoader();
+ /* First init pulls in embed.js (same as Cal’s generated snippet) */
+ window.Cal('init', 'consult', { origin: CAL_ORIGIN });
+ calApiReady = waitForEmbedScript();
+ }
+ return calApiReady;
+}
+
+function mountInline(slot) {
+ const targetId = slot.dataset.calTarget;
+ const themeKey = slot.dataset.calTheme === 'light' ? 'light' : 'dark';
+ const spec = SLOTS[themeKey];
+ const el = document.getElementById(targetId);
+ if (!el || !spec || !window.Cal?.ns) return;
+
+ window.Cal('init', spec.ns, { origin: CAL_ORIGIN });
+
+ window.Cal.ns[spec.ns]('inline', {
+ elementOrSelector: `#${targetId}`,
+ config: spec.inlineConfig,
+ calLink: CAL_LINK,
+ });
+
+ window.Cal.ns[spec.ns]('ui', spec.ui);
+}
+
+function watchSlot(slot) {
+ const targetId = slot.dataset.calTarget;
+ const el = document.getElementById(targetId);
+ if (!el) return;
+
+ const showFallback = () => {
+ slot.classList.add('is-blocked');
+ slot.classList.remove('is-embedded');
+ };
+ const showEmbed = () => {
+ slot.classList.remove('is-blocked');
+ slot.classList.add('is-embedded');
+ };
+
+ showFallback();
+
+ const hasLiveEmbed = () => {
+ const iframe = el.querySelector('iframe');
+ return Boolean(iframe && iframe.offsetHeight > 60);
+ };
+
+ const obs = new MutationObserver(() => {
+ if (hasLiveEmbed()) showEmbed();
+ });
+ obs.observe(el, { childList: true, subtree: true });
+
+ window.setTimeout(() => {
+ obs.disconnect();
+ if (!hasLiveEmbed()) showFallback();
+ }, 8000);
+}
+
+export function initCalEmbeds() {
+ const slots = [...document.querySelectorAll('[data-cal-embed]')];
+ if (!slots.length) return;
+
+ loadCalApi()
+ .then((Cal) => {
+ if (!Cal) return;
+ slots.forEach((slot) => {
+ mountInline(slot);
+ watchSlot(slot);
+ });
+ })
+ .catch(() => {
+ /* screenshots + ↗ */
+ });
+}
diff --git a/stack-folder/folder-rail.js b/stack-folder/folder-rail.js
new file mode 100644
index 0000000..4f882e4
--- /dev/null
+++ b/stack-folder/folder-rail.js
@@ -0,0 +1,32 @@
+/** Fixed rail — all L0–L7 visible; click to jump (syncs with scroll) */
+export function initRailRoller(rail = document.querySelector('.tab-rail')) {
+ const tabs = rail ? [...rail.querySelectorAll('.rail-tab')] : [];
+ if (!rail || !tabs.length) return;
+
+ function setActive(layer) {
+ const n = Math.max(0, Math.min(tabs.length - 1, layer));
+ tabs.forEach((btn, i) => btn.classList.toggle('active', i === n));
+ }
+
+ function gotoLayer(layer) {
+ setActive(layer);
+ window.dispatchEvent(
+ new CustomEvent('stack-goto-layer', { detail: { layer } }),
+ );
+ }
+
+ tabs.forEach((btn) => {
+ btn.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ gotoLayer(Number(btn.dataset.layer ?? btn.dataset.goto));
+ });
+ });
+
+ window.addEventListener('stack-depth', (e) => {
+ const layer = Number(e.detail?.active);
+ if (Number.isFinite(layer)) setActive(layer);
+ });
+
+ setActive(0);
+}
diff --git a/stack-folder/folder.css b/stack-folder/folder.css
index 39d3ff6..0cdd0d4 100644
--- a/stack-folder/folder.css
+++ b/stack-folder/folder.css
@@ -2,142 +2,687 @@
@import '../shared/stack-layout.css';
:root {
- --tab-offset: 2.35rem;
+ --tab-offset: 2.1rem;
--mono: 'IBM Plex Mono', monospace;
--sans: 'Instrument Sans', system-ui, sans-serif;
+ --text: 'Instrument Sans', system-ui, sans-serif;
+ --display: 'Literata', Georgia, serif;
+ --hi: #fff59d;
+ --hi-edge: #f5e06a;
+ --desk: #3d3830;
+ --manila: #ebe4d4;
+ --manila-edge: #c9bea8;
+ --manila-dark: #d8cfbc;
+ --ink: #1c1a16;
+ --ink-muted: #4a4640;
+ --l0-tab: #c9a86c;
+ --l0-tab-fg: #2a2824;
+ --l1-tab: #a8c4d4;
+ --l1-tab-fg: #1a2830;
+ --l2-tab: #b8d4a8;
+ --l2-tab-fg: #1a2818;
+ --l3-tab: #d4b8c4;
+ --l3-tab-fg: #2a1820;
+ --l4-tab: #d4c8a8;
+ --l4-tab-fg: #2a2418;
+ --l5-tab: #c4c4c4;
+ --l5-tab-fg: #2a2a2a;
+ --l6-tab: #6a7a8a;
+ --l6-tab-fg: #f0ece4;
+ --l7-tab: #2a4a6b;
+ --l7-tab-fg: #ebe4d4;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
-body { font-family: var(--sans); background: #2a2824; color: #1a1814; }
-
-.nav {
- position: fixed; top: 0; left: 0; right: 0; z-index: 400;
- display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; padding: 0.5rem 1rem;
- font-family: var(--mono); font-size: 0.62rem;
- background: rgba(42,40,36,0.97); color: #c4b8a8;
+body {
+ font-family: var(--sans);
+ background: var(--desk);
+ color: var(--ink);
+ background-image:
+ linear-gradient(90deg, rgba(0,0,0,0.04) 1px, transparent 1px),
+ linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px);
+ background-size: 24px 24px;
}
-.nav a { color: #8a8278; text-decoration: none; }
-.nav a:hover { color: #d4a574; }
-.depth { margin-left: auto; color: #d4a574; font-weight: 600; }
+.site-header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 400;
+ padding: 0.5rem 1rem;
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ letter-spacing: 0.02em;
+ text-align: center;
+ color: #b8ad9d;
+ background: rgba(42, 40, 36, 0.97);
+ border-bottom: 1px solid #59534a;
+}
+
+.site-header a {
+ color: #d4a574;
+ text-decoration: none;
+}
+
+.site-header a:hover {
+ color: #ebc39a;
+}
.tab-rail {
position: fixed;
- top: 2.75rem;
- right: max(0.5rem, calc(50% - 340px));
+ top: calc(var(--stack-nav) + 0.35rem);
+ left: calc(50% + 20rem + 1.35rem);
+ right: auto;
z-index: 500;
display: flex;
flex-direction: column;
- gap: 0.2rem;
- padding: 0.35rem;
- background: rgba(42,40,36,0.92);
+ gap: 0.15rem;
+ padding: 0.3rem;
+ background: rgba(42,40,36,0.94);
border: 1px solid #4a4844;
- border-radius: 6px;
+ border-radius: 0;
}
.rail-tab {
font-family: var(--mono);
font-size: 0.55rem;
- padding: 0.2rem 0.45rem;
- border: none;
- border-radius: 3px;
+ padding: 0.22rem 0.5rem;
+ border: 1px solid transparent;
+ border-radius: 0;
background: transparent;
color: #8a8278;
cursor: pointer;
text-align: left;
+ min-width: 2.1rem;
}
-.rail-tab:hover,
-.rail-tab.active { background: #c9a86c; color: #2a2824; }
+.rail-tab[data-layer="0"]:hover,
+.rail-tab[data-layer="0"].active { background: var(--l0-tab); color: var(--l0-tab-fg); border-color: rgba(0,0,0,0.12); }
+.rail-tab[data-layer="1"]:hover,
+.rail-tab[data-layer="1"].active { background: var(--l1-tab); color: var(--l1-tab-fg); border-color: rgba(0,0,0,0.12); }
+.rail-tab[data-layer="2"]:hover,
+.rail-tab[data-layer="2"].active { background: var(--l2-tab); color: var(--l2-tab-fg); border-color: rgba(0,0,0,0.12); }
+.rail-tab[data-layer="3"]:hover,
+.rail-tab[data-layer="3"].active { background: var(--l3-tab); color: var(--l3-tab-fg); border-color: rgba(0,0,0,0.12); }
+.rail-tab[data-layer="4"]:hover,
+.rail-tab[data-layer="4"].active { background: var(--l4-tab); color: var(--l4-tab-fg); border-color: rgba(0,0,0,0.12); }
+.rail-tab[data-layer="5"]:hover,
+.rail-tab[data-layer="5"].active { background: var(--l5-tab); color: var(--l5-tab-fg); border-color: rgba(0,0,0,0.12); }
+.rail-tab[data-layer="6"]:hover,
+.rail-tab[data-layer="6"].active { background: var(--l6-tab); color: var(--l6-tab-fg); border-color: rgba(0,0,0,0.12); }
+.rail-tab[data-layer="7"]:hover,
+.rail-tab[data-layer="7"].active { background: var(--l7-tab); color: var(--l7-tab-fg); border-color: rgba(0,0,0,0.08); }
.mount {
+ --folder-body-h: 30rem;
width: min(640px, 100%);
margin: 0 auto;
- padding: var(--stack-nav) 1rem 0;
+ padding: calc(var(--stack-nav) + 0.75rem) 1rem 2rem;
+}
+
+/* Scroll step between layers (folder uses display:contents) */
+.folder:not(.folder--last) .body {
+ margin-bottom: calc(var(--stack-scroll-slot) + 5.5rem);
+}
+
+.folder--last .body {
+ margin-bottom: var(--stack-runway, 0px);
}
-/* Same pattern as /stack/ — one sticky card, next covers previous */
.folder {
+ display: contents;
+}
+
+.folder .body {
position: sticky;
- min-height: var(--stack-card-min);
- margin-bottom: 1.25rem;
- width: 100%;
+ top: calc(var(--stack-stick) + var(--stack-tab-h) - 1px);
+ cursor: pointer;
}
+.tab {
+ opacity: 1;
+}
+
+/* Each tab sticks above its folder body as you scroll the stack */
.tab {
position: sticky;
display: block;
- width: fit-content;
- max-width: calc(100% - 1rem);
+ top: var(--stack-stick);
+ height: var(--stack-tab-h);
+ box-sizing: border-box;
+ width: max-content;
+ max-width: none;
+ white-space: nowrap;
font-family: var(--mono);
- font-size: 0.62rem;
+ font-size: 0.58rem;
font-weight: 600;
letter-spacing: 0.04em;
- padding: 0.4rem 1rem 0.35rem;
- border: 1px solid rgba(0,0,0,0.12);
+ text-transform: uppercase;
+ line-height: 1.2;
+ padding: 0.28rem 0.5rem 0;
+ border: 1px solid rgba(0,0,0,0.18);
border-bottom: none;
- border-radius: 8px 8px 0 0;
+ border-radius: 0;
cursor: pointer;
text-align: left;
- z-index: 80;
- box-shadow: 0 -2px 8px rgba(0,0,0,0.12);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.35);
}
-.tab:hover { filter: brightness(1.06); }
+.tab-label { font-weight: 500; opacity: 0.92; }
+.tab:hover { filter: brightness(1.04); }
+
+/* Every folder same height — previews fill remaining space */
.body {
- background: #e8e2d4;
- border: 1px solid #c4b8a8;
- border-top: none;
- border-radius: 0 10px 10px 10px;
- padding: 1.25rem 1.4rem 1.5rem;
- box-shadow: 0 10px 32px rgba(0,0,0,0.25);
+ min-height: var(--folder-body-h);
+ height: var(--folder-body-h);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ font-family: var(--sans);
+ font-size: 0.88rem;
+ font-weight: 400;
+ line-height: 1.55;
+ background: linear-gradient(180deg, var(--manila) 0%, #e3dbc8 100%);
+ border: 1px solid var(--manila-edge);
+ border-top: 2px solid var(--manila-dark);
+ border-radius: 0;
+ padding: 0.85rem 1rem 0.95rem 1.15rem;
+ box-shadow:
+ inset 5px 0 0 var(--manila-dark),
+ inset 0 -1px 0 rgba(0,0,0,0.06);
}
-.f0 { top: var(--stack-stick); z-index: 1; }
-.f0 .tab { top: var(--stack-stick); margin-left: calc(var(--tab-offset) * 0); background: #c9a86c; color: #2a2824; }
+.body-copy {
+ flex-shrink: 0;
+ overflow: hidden;
+}
-.f1 { top: calc(var(--stack-stick) + var(--stack-step)); z-index: 2; }
-.f1 .tab { top: calc(var(--stack-stick) + var(--stack-step)); margin-left: calc(var(--tab-offset) * 1); background: #a8c4d4; color: #1a2830; }
+.body-copy h2 {
+ margin-bottom: 0.25rem;
+}
-.f2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); z-index: 3; }
-.f2 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 2); margin-left: calc(var(--tab-offset) * 2); background: #b8d4a8; color: #1a2818; }
+.body-copy p:last-child {
+ margin-bottom: 0.35rem;
+}
-.f3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); z-index: 4; }
-.f3 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 3); margin-left: calc(var(--tab-offset) * 3); background: #d4b8c4; color: #2a1820; }
+.body-fill {
+ flex: 1 1 auto;
+ min-height: 0;
+ pointer-events: none;
+}
-.f4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); z-index: 5; }
-.f4 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 4); margin-left: calc(var(--tab-offset) * 4); background: #d4c8a8; color: #2a2418; }
+/* Preview folders: text on top, screenshot fills the rest */
+.body--has-preview {
+ padding-bottom: 0.7rem;
+ overflow: hidden;
+}
-.f5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); z-index: 6; }
-.f5 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 5); margin-left: calc(var(--tab-offset) * 5); background: #c4c4c4; color: #2a2a2a; }
+.body--has-preview .body-copy {
+ max-height: 38%;
+ overflow: hidden;
+ overflow-y: hidden;
+}
-.f6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); z-index: 7; margin-bottom: 4rem; }
-.f6 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 6); margin-left: calc(var(--tab-offset) * 6); background: #2a4a6b; color: #e8e2d4; }
+.body--has-preview .body-copy p {
+ margin-bottom: 0.3rem;
+ font-size: 0.86rem;
+ line-height: 1.5;
+}
+
+.body--has-preview .body-copy h1 {
+ font-size: 1.2rem;
+ margin-bottom: 0.2rem;
+}
+
+.body--has-preview .body-copy h2 {
+ font-size: 1.05rem;
+ margin-bottom: 0.2rem;
+}
+
+.body--has-preview .body-copy .cta-block {
+ margin-top: 0.35rem;
+}
+
+.body--has-preview .terms-list {
+ margin-bottom: 0.25rem;
+ font-size: 0.95rem;
+}
+
+.body--has-preview .terms-list li {
+ margin-bottom: 0.2rem;
+}
+
+/* Screenshot preview — click opens site in new tab */
+a.site-preview {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-height: 0;
+ margin: 0.4rem 0 0;
+ border: 1px solid var(--manila-edge);
+ background: #f6f2e8;
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.5);
+ text-decoration: none;
+ color: inherit;
+ cursor: pointer;
+ overflow: hidden;
+}
+
+a.site-preview:hover {
+ border-color: #a89878;
+ filter: brightness(1.02);
+}
+
+.site-preview-bar {
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ padding: 0.3rem 0.45rem;
+ background: #ddd6c8;
+ border-bottom: 1px solid var(--manila-edge);
+ font-family: var(--mono);
+ font-size: 0.52rem;
+ color: var(--ink-muted);
+}
+
+.site-preview-url {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.site-preview-go {
+ color: #2a4a6b;
+ font-weight: 600;
+}
+
+.site-preview-frame {
+ display: flex;
+ flex: 1 1 0;
+ min-height: 11rem;
+ overflow: hidden;
+ background: #1a1814;
+}
+
+.site-preview-img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ min-height: 100%;
+ object-fit: cover;
+ object-position: top center;
+ background: #1a1814;
+}
+
+.cta-block {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.45rem;
+ margin: 0.55rem 0 0.35rem;
+}
+
+.body .btn {
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ font-weight: 600;
+ line-height: 1.2;
+ letter-spacing: 0.01em;
+}
+
+.body .btn--primary {
+ background: #2a4a6b;
+ color: #fff !important;
+ border: 1px solid #1e3854;
+}
+
+.body .btn--primary:hover {
+ background: #1e3854;
+ color: #fff !important;
+}
+
+.body .btn--ghost {
+ background: #f6f2e8;
+ color: #1c3a5c !important;
+ border: 1px solid #8a9ab0;
+}
+
+.body .btn--ghost:hover {
+ background: #fff;
+ color: #2a4a6b !important;
+}
+
+.service-deep-list {
+ margin: 0 0 0.5rem 0;
+ padding: 0;
+ list-style: none;
+ font-size: 1.05rem;
+ line-height: 1.4;
+ color: var(--ink);
+}
+
+.service-deep-list > li {
+ margin-bottom: 0.55rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+}
+
+.service-deep-list > li:last-child {
+ border-bottom: none;
+ margin-bottom: 0.35rem;
+}
+
+.service-deep-list strong {
+ display: inline;
+ font-family: var(--sans);
+ font-size: inherit;
+ font-weight: 600;
+ color: var(--ink);
+}
+
+.hi {
+ font-weight: 600;
+ color: var(--ink);
+}
+
+.service-deep-list .xref {
+ display: block;
+ margin-top: 0.2rem;
+}
+
+.section-lead {
+ font-size: 0.88rem;
+ color: var(--ink-muted);
+ margin-bottom: 0.55rem;
+}
+
+/* L2 — dense copy, no internal scroll */
+.folder--services .body-copy {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: hidden;
+ overflow-y: hidden;
+}
+
+.folder--services .section-lead {
+ font-size: 0.86rem;
+ margin-bottom: 0.4rem;
+ line-height: 1.4;
+}
+
+.folder--services .service-deep-list {
+ font-size: 0.8rem;
+ line-height: 1.35;
+}
+
+.folder--services .service-deep-list > li {
+ margin-bottom: 0.35rem;
+ padding-bottom: 0.3rem;
+}
+
+.folder--services .service-deep-list strong {
+ font-size: 0.88rem;
+ display: inline;
+ margin-right: 0.2rem;
+}
+
+.folder--services .service-deep-list .xref {
+ display: inline;
+ margin-top: 0;
+ margin-left: 0.15rem;
+}
+
+.folder--services .body-copy > p:last-child {
+ font-size: 0.78rem;
+ margin-bottom: 0;
+}
+
+.body::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 28%;
+ height: 100%;
+ pointer-events: none;
+ background: linear-gradient(90deg, transparent, rgba(0,0,0,0.03));
+}
+
+/* Bodies pile by z-index; each tab sticks above its folder */
+.f0 .body { z-index: 1; }
+.f1 .body { z-index: 2; }
+.f2 .body { z-index: 3; }
+.f3 .body { z-index: 4; }
+.f4 .body { z-index: 5; }
+.f5 .body { z-index: 6; }
+.f6 .body { z-index: 7; }
+.f7 .body { z-index: 8; }
+
+.f0 .tab { margin-left: calc(var(--tab-offset) * 0); z-index: 110; background: var(--l0-tab); color: var(--l0-tab-fg); }
+.f1 .tab { margin-left: calc(var(--tab-offset) * 1); z-index: 111; background: var(--l1-tab); color: var(--l1-tab-fg); }
+.f2 .tab { margin-left: calc(var(--tab-offset) * 2); z-index: 112; background: var(--l2-tab); color: var(--l2-tab-fg); }
+.f3 .tab { margin-left: calc(var(--tab-offset) * 3); z-index: 113; background: var(--l3-tab); color: var(--l3-tab-fg); }
+.f4 .tab { margin-left: calc(var(--tab-offset) * 4); z-index: 114; background: var(--l4-tab); color: var(--l4-tab-fg); }
+.f5 .tab { margin-left: calc(var(--tab-offset) * 5); z-index: 115; background: var(--l5-tab); color: var(--l5-tab-fg); }
+.f6 .tab { margin-left: calc(var(--tab-offset) * 6); z-index: 116; background: var(--l6-tab); color: var(--l6-tab-fg); }
+.f7 .tab { margin-left: calc(var(--tab-offset) * 7); z-index: 117; background: var(--l7-tab); color: var(--l7-tab-fg); }
+
+.file-id {
+ font-family: var(--mono);
+ font-size: 0.58rem;
+ letter-spacing: 0.06em;
+ color: var(--ink-muted);
+ margin-bottom: 0.35rem;
+}
+
+.body h1 {
+ font-family: var(--display);
+ font-size: 1.35rem;
+ font-weight: 600;
+ line-height: 1.25;
+ margin-bottom: 0.35rem;
+ color: var(--ink);
+}
+
+.body h2 {
+ font-family: var(--display);
+ font-size: 1.05rem;
+ font-weight: 600;
+ margin-bottom: 0.4rem;
+ color: var(--ink);
+}
+
+.lead, .body p, .clause {
+ font-size: 0.88rem;
+ color: var(--ink-muted);
+ line-height: 1.55;
+ margin-bottom: 0.45rem;
+}
+
+.stack-note, .xref {
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ color: var(--ink-muted);
+}
+
+.body a:not(.btn) { color: #2a4a6b; font-weight: 600; }
+
+.meta-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.8rem;
+ margin: 0.5rem 0 0.65rem;
+}
+
+.meta-table th {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ font-weight: 500;
+ text-align: left;
+ color: var(--ink-muted);
+ padding: 0.2rem 0.65rem 0.2rem 0;
+ vertical-align: top;
+ width: 5.5rem;
+}
+
+.meta-table td { padding: 0.2rem 0; color: var(--ink); }
+
+.badge {
+ display: inline-block;
+ font-family: var(--mono);
+ font-size: 0.58rem;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ padding: 0.12rem 0.4rem;
+ border: 1px solid var(--ink);
+ color: var(--ink);
+}
+
+.badge--avail { border-color: #3d6b3d; color: #2d5a2d; }
+
+.avail { margin-top: 0.35rem; font-size: 0.82rem; }
+
+.terms-list {
+ margin: 0 0 0.65rem 1rem;
+ font-size: 1.05rem;
+ color: var(--ink);
+ line-height: 1.45;
+}
+
+.terms-list strong {
+ font-weight: 600;
+ color: var(--ink);
+}
+
+.terms-list li { margin-bottom: 0.35rem; }
+
+.meta-table--contact { margin-top: 0.5rem; }
-.body h1 { font-size: 1.65rem; margin-bottom: 0.35rem; }
-.body h2 { font-size: 1.25rem; margin-bottom: 0.4rem; }
-.body p { font-size: 0.92rem; color: #4a4844; line-height: 1.5; }
-.body a { color: #2a4a6b; }
-.avail { font-family: var(--mono); font-size: 0.68rem; color: #3d6b3d; margin-top: 0.5rem; }
.btn {
- font-family: var(--mono); font-size: 0.7rem; padding: 0.45rem 0.75rem;
- background: #2a4a6b; color: #fff; text-decoration: none; border-radius: 4px;
- margin-right: 0.4rem; display: inline-block; margin-top: 0.35rem;
+ padding: 0.5rem 0.85rem;
+ text-decoration: none;
+ border-radius: 0;
+ margin-right: 0.35rem;
+ display: inline-block;
+ margin-top: 0.3rem;
}
-.btn.ghost { background: transparent; color: #2a4a6b; border: 1px solid #2a4a6b; }
-.foot {
- display: flex; justify-content: space-between;
- width: min(640px, 100%); margin: 0 auto;
- padding: 0 1rem 2rem; font-family: var(--mono); font-size: 0.62rem; color: #6a6458;
+/* Cal.com embed (needs embed allowlist on cal.levkin.ca for levkin.ca) */
+.cal-slot {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-height: 0;
+ margin: 0.4rem 0 0;
+ border: 1px solid var(--manila-edge);
+ background: #1a1814;
+ overflow: hidden;
+}
+
+.cal-slot .site-preview-bar {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ padding: 0.3rem 0.45rem;
+ background: #ddd6c8;
+ border-bottom: 1px solid var(--manila-edge);
+ font-family: var(--mono);
+ font-size: 0.52rem;
+ color: var(--ink-muted);
+}
+
+.cal-slot .site-preview-go {
+ color: #2a4a6b;
+ font-weight: 600;
+ text-decoration: none;
+}
+
+.cal-embed-frame {
+ position: relative;
+ flex: 1 1 auto;
+ min-height: 11rem;
+ background: #0a0a0a;
+}
+
+.cal-slot[data-cal-theme='light'] .cal-embed-frame {
+ background: #f5f3ee;
+}
+
+.cal-inline {
+ width: 100%;
+ height: 100%;
+ min-height: 14rem;
+ overflow: auto;
+}
+
+.cal-slot.is-blocked .cal-inline {
+ display: none;
+}
+
+.cal-slot.is-embedded .cal-inline iframe {
+ display: block;
+ width: 100%;
+ min-height: 14rem;
+ border: 0;
+}
+
+.cal-embed-fallback {
+ display: none;
+ position: absolute;
+ inset: 0;
+ text-decoration: none;
+ color: inherit;
+}
+
+.cal-slot.is-blocked .cal-embed-fallback {
+ display: flex;
+ flex-direction: column;
+}
+
+.cal-embed-fallback img {
+ flex: 1;
+ width: 100%;
+ object-fit: cover;
+ object-position: top center;
+ /* Crop Cal.com branding bar at bottom of screenshot */
+ max-height: calc(100% + 2rem);
+ margin-bottom: -2rem;
+}
+
+.cal-embed-fallback-label {
+ font-family: var(--mono);
+ font-size: 0.55rem;
+ text-align: center;
+ padding: 0.35rem;
+ background: rgba(0, 0, 0, 0.75);
+ color: #c4b8a8;
+}
+
+.service-note {
+ font-size: 0.78rem;
+ line-height: 1.4;
+ color: var(--ink-muted);
+ margin-bottom: 0.28rem;
+}
+
+.folder--services .service-deep-list > li {
+ margin-bottom: 0.28rem;
+ padding-bottom: 0.22rem;
+}
+
+.folder--services .body-copy > p:last-child {
+ margin-bottom: 0;
}
-.foot a { color: #6a6458; text-decoration: none; }
@media (max-width: 720px) {
.tab-rail { display: none; }
- :root { --stack-step: 1.75rem; --tab-offset: 1.4rem; --stack-card-min: 48vh; }
- .f5 .tab { margin-left: calc(var(--tab-offset) * 4); }
- .f6 .tab { margin-left: calc(var(--tab-offset) * 3); }
+ .mount { padding-right: 1rem; }
+ :root { --tab-offset: 1.75rem; }
}
+
diff --git a/stack-folder/index.html b/stack-folder/index.html
index 3fab136..892bb42 100644
--- a/stack-folder/index.html
+++ b/stack-folder/index.html
@@ -3,21 +3,21 @@
-
Levkin — Stack Folder
+
Levkin — Company Files
+
-
+
+
+
-
+
-
+
@@ -25,74 +25,161 @@
+
-
-
-
Levkin
-
Software development · Canada · remote
-
Taking new engagements · 15+ yrs · 8h→2m
+
+
+
+
LK-SPEC-1.0 · Cover sheet
+
Levkin Software Development Company
+
Custom applications, automation, and practice systems — scoped, documented, and built to run in production.
+
AVAILABLE Taking on new engagements · remote NA & EU
+
+
+
+
cal.levkin.ca · book a call↗
+
+
-
-
-
Custom software
-
Web apps, APIs, tools — TypeScript · Python · .NET · PostgreSQL
+
+
+
+
Scope
+
Boutique practice — not a staffing agency. Clear deliverable and handoff so your team can own what ships.
+
+
+ levkin.ca / spec↗
+
+
-
-
+
+
-
Automation
-
n8n · Zapier · CI/CD · LLMs · auto.levkin.ca
+
+
Services
+
Primary lines — each engagement gets a statement of work after discovery.
+
+ - Custom software — Web apps, APIs, and internal tools; fit the problem, not a preset stack.
+ - Automation — Reporting, notifications, data sync, ops in the background. auto.levkin.ca
+ - CaseWare — Templates, releases, practice customization, pipeline work. caseware.levkin.ca
+ - Quality & testing — Test strategy, automation, release confidence. iliadobkin.com
+
+
How we work: fixed scope · production-ready · handoff-friendly · right-sized solutions.
+
Discovery call → written proposal → delivery with docs → optional support.
+
+
-
-
-
CaseWare & CaseView
-
15+ years · CaseWare Intl, MNP, JazzIt · caseware.levkin.ca
+
+
+
+
Automation
+
Workflows that run reliably in the background — reporting, sync, ops off your plate.
+
+
+ auto.levkin.ca↗
+
+
-
-
-
Quality engineering
-
Senior SDET · iliadobkin.com
+
+
+
+
CaseWare
+
Templates, releases, and practice customization — from small fixes to full pipeline overhauls.
+
+
+ caseware.levkin.ca↗
+
+
-
-
-
Job Ops
-
Internal hiring orchestrator · jobs.levkin.ca
+
+
+
+
Quality & testing
+
Test strategy, automation, and release confidence without hiring full-time.
+
+
+ iliadobkin.com↗
+
+
-
-
-
Engage
-
Discover → Proposal → Ship → Maintain
-
Book 15 min hello@levkine.ca
+
+
+
+
Source & repos
+
Project code and templates on our Gitea — private by default, yours on handoff.
+
+
+ git.levkin.ca / explore↗
+
+
-
-
+
+
+
+
+
Terms & contact
+
+ - Fixed scope — quoted after discovery; no open-ended hourly surprises.
+ - Production-ready — monitoring, failure handling, documentation included.
+ - Handoff-friendly — your team or the next vendor can maintain what we ship.
+
+
+
+
cal.levkin.ca · book a call↗
+
+
+
+
-
+
+
diff --git a/stack-folder/previews/auto.png b/stack-folder/previews/auto.png
new file mode 100644
index 0000000..e7145f3
Binary files /dev/null and b/stack-folder/previews/auto.png differ
diff --git a/stack-folder/previews/cal-dark.png b/stack-folder/previews/cal-dark.png
new file mode 100644
index 0000000..a6a5253
Binary files /dev/null and b/stack-folder/previews/cal-dark.png differ
diff --git a/stack-folder/previews/cal-light.png b/stack-folder/previews/cal-light.png
new file mode 100644
index 0000000..02c5dc3
Binary files /dev/null and b/stack-folder/previews/cal-light.png differ
diff --git a/stack-folder/previews/cal.png b/stack-folder/previews/cal.png
new file mode 100644
index 0000000..7d4de20
Binary files /dev/null and b/stack-folder/previews/cal.png differ
diff --git a/stack-folder/previews/caseware.png b/stack-folder/previews/caseware.png
new file mode 100644
index 0000000..d1c18f3
Binary files /dev/null and b/stack-folder/previews/caseware.png differ
diff --git a/stack-folder/previews/git-repos.png b/stack-folder/previews/git-repos.png
new file mode 100644
index 0000000..f070c78
Binary files /dev/null and b/stack-folder/previews/git-repos.png differ
diff --git a/stack-folder/previews/git.png b/stack-folder/previews/git.png
new file mode 100644
index 0000000..663e6f3
Binary files /dev/null and b/stack-folder/previews/git.png differ
diff --git a/stack-folder/previews/iliadobkin.png b/stack-folder/previews/iliadobkin.png
new file mode 100644
index 0000000..3a06934
Binary files /dev/null and b/stack-folder/previews/iliadobkin.png differ
diff --git a/stack-folder/previews/jobs.png b/stack-folder/previews/jobs.png
new file mode 100644
index 0000000..abed536
Binary files /dev/null and b/stack-folder/previews/jobs.png differ
diff --git a/stack-folder/previews/spec.png b/stack-folder/previews/spec.png
new file mode 100644
index 0000000..245fb1b
Binary files /dev/null and b/stack-folder/previews/spec.png differ
diff --git a/stack-folder/previews/stack.png b/stack-folder/previews/stack.png
new file mode 100644
index 0000000..c8246df
Binary files /dev/null and b/stack-folder/previews/stack.png differ
diff --git a/stack/index.html b/stack/index.html
index 8df7548..0dea7ac 100644
--- a/stack/index.html
+++ b/stack/index.html
@@ -4,7 +4,7 @@
Levkin — Stack
-
+
@@ -14,72 +14,70 @@
-
-
Levkin
-
Software · Canada · remote
-
Boutique engineering — production systems, automation, enterprise.
-
15+ yrs8h→2m24/7
-
Taking new engagements
+
+
Levkin Inc.
+
LK-SPEC-1.0 · Software development · Canada
+
Builds and maintains production software — custom apps, automation, practice systems. Scoped, documented, handoff-ready.
+
ACTIVE · Taking new engagements · remote NA & EU
-
-
Custom software
-
Web apps, APIs, internal tools — TS · Python · .NET
+
+
1. Scope
+
Boutique practice — clear start, deliverable, and handoff. Discovery → fixed proposal → delivery → optional support.
-
-
Automation
-
n8n · Zapier · CI/CD · webhooks · LLMs
+
+
2.1 Custom software
+
Web apps, APIs, internal tools — chosen to fit the problem, not a preset stack.
-
-
CaseWare & CaseView
-
15+ years · CaseWare Intl, MNP, JazzIt
+
+
2.2 Automation
+
Background workflows — reporting, notifications, data sync, ops off your plate.
-
-
Quality engineering
-
Senior SDET · test automation · CI/CD · trace-driven QA
+
+
2.3 CaseWare
+
Templates, releases, practice customization — small fixes to full pipeline overhauls.
-
-
Job Ops
-
Internal hiring orchestrator (auth required)
+
+
2.4 Quality & testing
+
Test strategy, automation, release confidence — experienced QA lead without full-time hire.
-
-
Engage
-
Discover → Proposal → Ship → Maintain
+
+
Terms & contact
+
Fixed scope · production-ready · handoff-friendly · right-sized.
-
Retries · docs · tests first
diff --git a/stack/stack.css b/stack/stack.css
index 9eb614e..df68aaf 100644
--- a/stack/stack.css
+++ b/stack/stack.css
@@ -36,14 +36,23 @@ body {
margin: 0 auto;
}
-/* Each card sticks; next scrolls over previous */
+/* Compact sticky card — click brings to front */
+.layer:not(.layer-6) {
+ margin-bottom: var(--stack-scroll-slot);
+}
+
.layer {
position: sticky;
- min-height: var(--stack-card-min);
- margin-bottom: 1.25rem;
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; }
@@ -52,9 +61,9 @@ body {
.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: 4rem; }
+.layer-6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); z-index: 7; background: #4c4c5e; margin-bottom: 0; }
-.layer-inner { padding: 1.2rem 1.35rem 1.5rem; }
+.layer-inner { padding: 0.95rem 1.15rem 1.05rem; }
.layer-head {
display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.65rem;
@@ -70,8 +79,8 @@ body {
.layer-id:hover { text-decoration: underline; }
.layer-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: 2rem; font-weight: 600; letter-spacing: -0.03em; }
-.layer h2 { font-size: 1.2rem; font-weight: 600; margin-bottom: 0.35rem; }
+.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; }
@@ -105,6 +114,6 @@ body {
.stack-ruler button.active { color: #c4a574; }
@media (max-width: 700px) {
- :root { --stack-step: 1.75rem; --stack-card-min: 48vh; }
+ :root { --stack-step: 1.5rem; }
.variants { display: none; }
}
diff --git a/vite.config.js b/vite.config.js
index 16969da..0d0a149 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -7,13 +7,8 @@ export default defineConfig({
input: {
main: resolve(__dirname, 'index.html'),
spec: resolve(__dirname, 'spec/index.html'),
- slab: resolve(__dirname, 'slab/index.html'),
- relay: resolve(__dirname, 'relay/index.html'),
- vault: resolve(__dirname, 'vault/index.html'),
stack: resolve(__dirname, 'stack/index.html'),
stackFolder: resolve(__dirname, 'stack-folder/index.html'),
- stackTrace: resolve(__dirname, 'stack-trace/index.html'),
- stackRack: resolve(__dirname, 'stack-rack/index.html'),
},
},
},