Fix stack cover: single sticky folder unit, hide inactive bodies.

Tested all four variants in browser — only active layer body visible while
tabs stay staggered; scroll covers previous layers correctly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ilia 2026-05-20 23:11:57 -04:00
parent bb8469ca10
commit cfe1cf3922
14 changed files with 499 additions and 160 deletions

48
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "levkin.ca",
"version": "0.3.0",
"devDependencies": {
"playwright": "^1.60.0",
"vite": "^6.3.5"
}
},
@ -963,6 +964,53 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",

View File

@ -9,6 +9,7 @@
"preview": "vite preview"
},
"devDependencies": {
"playwright": "^1.60.0",
"vite": "^6.3.5"
}
}

34
scripts/debug-sticky.mjs Normal file
View File

@ -0,0 +1,34 @@
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto('http://localhost:5176/stack-folder/', { waitUntil: 'networkidle' });
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();

91
scripts/test-stack.mjs Normal file
View File

@ -0,0 +1,91 @@
import { chromium } from 'playwright';
import { writeFileSync, mkdirSync } from 'fs';
import { join } from 'path';
const BASE = process.env.BASE_URL || 'http://localhost:5176';
const OUT = '/tmp/levkin-stack-test';
mkdirSync(OUT, { recursive: true });
const pages = [
{ name: 'stack', path: '/stack/' },
{ name: 'stack-folder', path: '/stack-folder/' },
{ name: 'stack-trace', path: '/stack-trace/' },
{ name: 'stack-rack', path: '/stack-rack/' },
];
const scrollY = [0, 400, 800, 1200, 1800, 2400];
async function analyze(page, label) {
return page.evaluate(() => {
const sections = [...document.querySelectorAll('.scroll-section')];
const bodies = [...document.querySelectorAll('.body, .layer-inner, .frame-body, .unit-body')];
const tabs = [...document.querySelectorAll('.tab, .frame-line, .unit-head')];
const sticky = [...document.querySelectorAll('.tab, .body, .layer-inner, .frame-line, .frame-body, .unit-head, .unit-body')];
const rects = (els) => els.map((el, i) => {
const r = el.getBoundingClientRect();
const cs = getComputedStyle(el);
return {
i,
tag: el.className?.slice?.(0, 40) || el.tagName,
top: Math.round(r.top),
bottom: Math.round(r.bottom),
height: Math.round(r.height),
position: cs.position,
zIndex: cs.zIndex,
visible: r.height > 0 && r.bottom > 0 && r.top < innerHeight,
};
});
const visibleBodies = rects(bodies).filter((b) => b.visible && b.height > 80);
const mount = document.querySelector('.mount, .stack-mount');
const mountH = mount ? Math.round(mount.getBoundingClientRect().height) : 0;
return {
scrollY: Math.round(window.scrollY),
pageH: Math.round(document.documentElement.scrollHeight),
viewport: innerHeight,
sections: sections.length,
visibleBodyCount: visibleBodies.length,
visibleBodies,
mountDocHeight: mountH,
firstSectionTop: sections[0] ? Math.round(sections[0].getBoundingClientRect().top) : null,
lastSectionBottom: sections.at(-1) ? Math.round(sections.at(-1).getBoundingClientRect().bottom) : null,
stickyPositions: rects(sticky.filter((el) => getComputedStyle(el).position === 'sticky')).slice(0, 14),
};
});
}
const browser = await chromium.launch({ headless: true });
const report = [];
for (const { name, path } of pages) {
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
const url = BASE + path;
await page.goto(url, { waitUntil: 'networkidle' });
const shots = [];
for (const y of scrollY) {
await page.evaluate((sy) => window.scrollTo(0, sy), y);
await page.waitForTimeout(200);
const data = await analyze(page, name);
const file = join(OUT, `${name}-scroll-${y}.png`);
await page.screenshot({ path: file, fullPage: false });
shots.push({ y, file, ...data });
}
report.push({ name, url, shots });
await page.close();
}
await browser.close();
const summary = report.map((r) => {
const problems = r.shots.map((s) => {
const issues = [];
if (s.visibleBodyCount > 2) issues.push(`${s.visibleBodyCount} bodies visible (want ≤2)`);
if (s.scrollY === 0 && s.visibleBodyCount > 1) issues.push('top: multiple full bodies');
if (s.pageH > s.viewport * 8) issues.push(`page very tall: ${s.pageH}px`);
return { y: s.y, issues, visibleBodyCount: s.visibleBodyCount, pageH: s.pageH };
});
return { variant: r.name, url: r.url, scrollChecks: problems };
});
writeFileSync(join(OUT, 'report.json'), JSON.stringify({ report, summary }, null, 2));
console.log(JSON.stringify(summary, null, 2));
console.log('\nScreenshots:', OUT);

View File

@ -18,3 +18,18 @@
height: 2rem;
margin-bottom: 3rem;
}
/* Only the active layer shows its body; tabs always visible */
.scroll-section:not(.is-active) .body,
.scroll-section:not(.is-active) .layer-inner,
.scroll-section:not(.is-active) .frame-body,
.scroll-section:not(.is-active) .unit-body {
visibility: hidden;
height: 0;
min-height: 0;
margin-top: 0 !important;
padding: 0;
border: none;
overflow: hidden;
box-shadow: none;
}

View File

@ -5,7 +5,7 @@ export function initStackScroll(options = {}) {
depthEl = document.getElementById('depth'),
depthPrefix = 'L',
tabSelector = '[data-goto], .jump',
bodySelector = '.body, .layer-inner, .frame-body, .unit-body',
panelSelector = '.folder, .layer, .frame, .unit',
} = options;
const sections = document.querySelectorAll(sectionSelector);
@ -29,9 +29,11 @@ export function initStackScroll(options = {}) {
sections.forEach((sec) => {
const layer = Number(sec.dataset.layer);
const body = sec.querySelector(bodySelector);
if (!body) return;
body.style.zIndex = layer === active ? 100 : 10 + layer;
const isActive = layer === active;
sec.classList.toggle('is-active', isActive);
const panel = sec.querySelector(panelSelector);
if (!panel) return;
panel.style.zIndex = isActive ? 100 : 10 + layer;
});
}

View File

@ -3,11 +3,10 @@
--stack-nav: 2.5rem;
--stack-stick: 3rem;
--stack-step: 2.75rem;
--stack-tab-h: 2.25rem;
/* Height of tab rail (all L0L6 tabs visible) */
--stack-tab-h: 2rem;
--stack-reveal: calc(var(--stack-stick) + var(--stack-step) * 6 + var(--stack-tab-h));
--stack-slot: 100vh;
--stack-slot-last: 50vh;
--stack-pull: calc(var(--stack-slot) - var(--stack-reveal));
--stack-body-h: calc(100dvh - var(--stack-reveal) - 1rem);
--stack-body-h: calc(100dvh - var(--stack-reveal) - 1.25rem);
}

View File

@ -77,7 +77,7 @@
<table class="rfc-meta">
<tbody>
<tr><th scope="row">Status</th><td><span class="badge">ACTIVE</span></td></tr>
<tr><th scope="row">Entity</th><td>Levkin</td></tr>
<tr><th scope="row">Entity</th><td>Levkin Inc.</td></tr>
<tr><th scope="row">Domain</th><td>levkin.ca</td></tr>
<tr><th scope="row">Updated</th><td><time datetime="2026-05-20">2026-05-20</time></td></tr>
</tbody>
@ -227,13 +227,14 @@
<span class="contact-path">/consult</span>
<span class="desc">15-minute discovery call<span class="sr-only"> (opens cal.levkin.ca)</span></span>
</a>
<a class="contact-card" href="mailto:hello@levkine.ca?subject=Project%20enquiry">
<a class="contact-card" href="mailto:ilia@levkine.ca?subject=Project%20enquiry">
<span class="method post" aria-hidden="true">POST</span>
<span class="contact-path">/email</span>
<span class="desc">hello@levkine.ca</span>
<span class="desc">ilia@levkine.ca</span>
</a>
</div>
<p class="copyright">© Levkin · Canadian software development</p>
<a href="#main" class="back-top">↑ Back to top</a>
</section>
</article>
</main>

View File

@ -1,66 +1,102 @@
/* --- themes --- */
/* --- themes (semantic tokens — no blue-on-blue panels) --- */
:root,
[data-theme="light"] {
color-scheme: light;
--paper: #f4f1ec;
--surface: #ffffff;
--ink: #1a1a18;
--muted: #4a4844;
--rule: #c8c2b8;
--accent: #1e4a72;
--accent-hover: #163a5c;
--accent-bg: #e4ecf4;
--link: #1e4a72;
--link-hover: #163a5c;
--paper: #f6f3ee;
--surface: #fffefb;
--panel: #ebe8e2;
--ink: #121211;
--muted: #3d3b37;
--rule: #c5bfb4;
--link: #0b4a75;
--link-hover: #083558;
--path: #0b4a75;
--nav-active: #e4e0d8;
--nav-active-border: #0b4a75;
--code-bg: #e8e5df;
--code-fg: #1a2e28;
--table-head: #e4e0d8;
--badge-ok-bg: #c8e6ce;
--badge-ok-fg: #14532d;
--badge-post-bg: #f5dcc8;
--badge-post-fg: #6b3410;
--focus: #1e4a72;
--skip-bg: #1a1a18;
--skip-fg: #f4f1ec;
--badge-ok-fg: #0f3d1a;
--badge-ok-border: #6b9e74;
--badge-post-bg: #f0e0d0;
--badge-post-fg: #5a2e0a;
--badge-post-border: #c49a6a;
--focus: #0b4a75;
--skip-bg: #121211;
--skip-fg: #f6f3ee;
}
[data-theme="dim"] {
color-scheme: light;
--paper: #ddd8cf;
--surface: #ece8e0;
--ink: #1f1d1a;
--muted: #45423c;
--rule: #b5aea2;
--accent: #2a5078;
--accent-hover: #1e3d5c;
--accent-bg: #d4dde8;
--link: #2a5078;
--link-hover: #1e3d5c;
--badge-ok-bg: #b8d4be;
--badge-ok-fg: #14532d;
--badge-post-bg: #e8cdb8;
--badge-post-fg: #5c3010;
--focus: #2a5078;
--skip-bg: #1f1d1a;
--skip-fg: #ece8e0;
--paper: #c9c3b8;
--surface: #e3ded4;
--panel: #d6d0c6;
--ink: #12110f;
--muted: #363430;
--rule: #a39d92;
--link: #094264;
--link-hover: #062f48;
--path: #094264;
--nav-active: #bab4a8;
--nav-active-border: #5c584f;
--code-bg: #d8d2c8;
--code-fg: #1a2822;
--table-head: #d0cac0;
--badge-ok-bg: #a8c8ae;
--badge-ok-fg: #0a3318;
--badge-ok-border: #4a7a54;
--badge-post-bg: #dcc8b4;
--badge-post-fg: #4a2808;
--badge-post-border: #a07850;
--focus: #094264;
--skip-bg: #12110f;
--skip-fg: #e3ded4;
}
[data-theme="dark"] {
color-scheme: dark;
--paper: #141412;
--surface: #1e1e1c;
--ink: #ece9e4;
--muted: #b8b4ac;
--paper: #10100f;
--surface: #1a1a18;
--panel: #242422;
--ink: #eeece8;
--muted: #c4c0b8;
--rule: #3a3834;
--accent: #7eb8e8;
--accent-hover: #a8d4f8;
--accent-bg: #243040;
--link: #8ec8f0;
--link-hover: #b8e0ff;
--badge-ok-bg: #1e4a2e;
--badge-ok-fg: #a8e8b8;
--badge-post-bg: #4a3020;
--badge-post-fg: #f0d0b0;
--focus: #8ec8f0;
--skip-bg: #ece9e4;
--skip-fg: #141412;
--link: #7ec8f4;
--link-hover: #a8e0ff;
--path: #9ed4f8;
--nav-active: #2a2a28;
--nav-active-border: #7ec8f4;
--code-bg: #1e1e1c;
--code-fg: #c0d8cc;
--table-head: #242422;
--badge-ok-bg: #1a3d28;
--badge-ok-fg: #b8e8c4;
--badge-ok-border: #4a8a5c;
--badge-post-bg: #3d2a1c;
--badge-post-fg: #f0d8c0;
--badge-post-border: #8a6a48;
--focus: #7ec8f4;
--skip-bg: #eeece8;
--skip-fg: #10100f;
}
@media (prefers-contrast: more) {
:root,
[data-theme="light"],
[data-theme="dim"] {
--muted: #1a1816;
--rule: #5a5650;
--badge-ok-border: #0f3d1a;
--badge-post-border: #5a2e0a;
}
[data-theme="dark"] {
--muted: #eeece8;
--rule: #8a8680;
--badge-ok-border: #b8e8c4;
--badge-post-border: #f0d8c0;
}
}
:root {
@ -137,6 +173,7 @@ input:focus-visible,
a {
color: var(--link);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.15em;
}
@ -145,6 +182,19 @@ a:hover {
color: var(--link-hover);
}
.rfc p a,
.spec-table a,
.endpoint a {
text-decoration-thickness: 1.5px;
}
.toc nav a,
.contact-card,
.back,
.theme-option span {
text-decoration: none;
}
@media (max-width: 800px) {
body { grid-template-columns: 1fr; }
.toc {
@ -180,7 +230,7 @@ a:hover {
line-height: 2.75rem;
}
.back:hover { color: var(--accent); }
.back:hover { color: var(--link); }
.toc-title {
font-size: 0.65rem;
@ -207,12 +257,14 @@ a:hover {
border-radius: 2px;
}
.toc nav a:hover { color: var(--accent); }
.toc nav a:hover { color: var(--link); }
.toc nav a.is-active {
font-weight: 600;
color: var(--accent);
background: var(--accent-bg);
color: var(--ink);
background: var(--nav-active);
padding: 0.35rem 0.5rem;
box-shadow: inset 0 0 0 1px var(--rule);
}
/* preferences */
@ -275,10 +327,10 @@ a:hover {
}
.theme-option input:checked + span {
border-color: var(--accent);
background: var(--accent-bg);
border-color: var(--nav-active-border);
background: var(--nav-active);
font-weight: 600;
color: var(--accent);
color: var(--ink);
}
.theme-option input:focus-visible + span {
@ -318,8 +370,8 @@ a:hover {
}
.font-btn:hover:not(:disabled) {
background: var(--accent-bg);
border-color: var(--accent);
background: var(--nav-active);
border-color: var(--nav-active-border);
}
.font-btn:disabled {
@ -395,12 +447,14 @@ a:hover {
padding: 0.15rem 0.45rem;
background: var(--badge-ok-bg);
color: var(--badge-ok-fg);
border: 1px solid var(--badge-ok-border);
letter-spacing: 0.04em;
}
.badge.muted {
background: var(--rule);
background: var(--panel);
color: var(--muted);
border-color: var(--rule);
}
section {
@ -422,7 +476,7 @@ section h2 {
section p { margin-bottom: 0.75rem; color: var(--ink); }
.code-block {
background: var(--accent-bg);
background: var(--code-bg);
border: 1px solid var(--rule);
padding: 1rem 1.25rem;
margin: 1rem 0;
@ -433,7 +487,7 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
font-family: var(--mono);
font-size: 0.8rem;
line-height: 1.5;
color: var(--accent);
color: var(--code-fg);
}
.endpoint {
@ -448,7 +502,7 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
align-items: center;
gap: 0.5rem 0.75rem;
padding: 0.75rem 1rem;
background: var(--accent-bg);
background: var(--panel);
border-bottom: 1px solid var(--rule);
font-family: var(--mono);
font-size: 0.8rem;
@ -458,11 +512,12 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
font-weight: 600;
color: var(--badge-ok-fg);
background: var(--badge-ok-bg);
border: 1px solid var(--badge-ok-border);
padding: 0.15rem 0.4rem;
font-size: 0.7rem;
}
.path { color: var(--accent); font-weight: 500; }
.path { color: var(--path); font-weight: 500; }
.ext { margin-left: auto; font-size: 0.75rem; }
.ext a { color: var(--muted); }
@ -504,14 +559,14 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
.spec-table th {
font-family: var(--mono);
font-size: 0.75rem;
background: var(--accent-bg);
background: var(--table-head);
font-weight: 500;
}
.spec-table code {
font-family: var(--mono);
font-size: 0.8rem;
color: var(--accent);
color: var(--code-fg);
}
.spec-table a { color: var(--link); }
@ -543,18 +598,32 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
.contact-card:hover,
.contact-card:focus-visible {
background: var(--accent-bg);
background: var(--panel);
}
.method.post {
font-weight: 600;
color: var(--badge-post-fg);
background: var(--badge-post-bg);
border: 1px solid var(--badge-post-border);
align-self: flex-start;
padding: 0.1rem 0.35rem;
font-size: 0.65rem;
}
.back-top {
display: inline-block;
margin-top: 1.5rem;
font-family: var(--mono);
font-size: 0.75rem;
color: var(--muted);
text-decoration: none;
}
.back-top:hover {
color: var(--link);
}
.contact-card .desc {
font-family: var(--serif);
font-size: 0.9rem;
@ -566,3 +635,43 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
color: var(--muted);
margin-top: 2rem;
}
@media print {
.skip-link,
.prefs,
.back {
display: none !important;
}
body {
display: block;
background: #fff;
color: #000;
}
.toc {
position: static;
height: auto;
border: none;
padding: 0 0 1rem;
}
.toc nav a.is-active {
font-weight: 700;
box-shadow: none;
}
a {
color: #000;
text-decoration: underline;
}
.rfc {
max-width: none;
padding: 0;
}
section {
break-inside: avoid;
}
}

View File

@ -19,9 +19,22 @@ function fontIndexFromScale(scale) {
return idx === -1 ? DEFAULT_FONT_INDEX : idx;
}
const THEME_COLORS = {
light: '#f6f3ee',
dim: '#c9c3b8',
dark: '#10100f',
};
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme === 'dark' ? 'dark' : theme === 'dim' ? 'light' : 'light';
document.documentElement.style.colorScheme = theme === 'dark' ? 'dark' : 'light';
let meta = document.querySelector('meta[name="theme-color"]');
if (!meta) {
meta = document.createElement('meta');
meta.name = 'theme-color';
document.head.appendChild(meta);
}
meta.content = THEME_COLORS[theme] || THEME_COLORS.light;
const input = document.querySelector(`input[name="theme"][value="${theme}"]`);
if (input) input.checked = true;
}

View File

@ -60,14 +60,14 @@ body { font-family: var(--sans); background: #2a2824; color: #1a1814; }
padding: var(--stack-nav) 1rem 0;
}
/* Whole folder sticks; tab staggered via folder top; bodies align to reveal */
.folder {
position: relative;
position: sticky;
width: 100%;
z-index: 10;
}
/* Tabs stick in staggered rail — always above folder bodies */
.tab {
position: sticky;
display: block;
width: fit-content;
max-width: calc(100% - 1rem);
@ -83,31 +83,47 @@ body { font-family: var(--sans); background: #2a2824; color: #1a1814; }
text-align: left;
box-shadow: 0 -2px 8px rgba(0,0,0,0.12);
transition: filter 0.15s;
z-index: 60;
}
.tab:hover { filter: brightness(1.06); }
/* Bodies share one stick line; active layer z-index set in JS */
.body {
position: sticky;
top: var(--stack-reveal);
background: #e8e2d4;
border: 1px solid #c4b8a8;
border-top: none;
border-radius: 0 10px 10px 10px;
padding: 1.25rem 1.4rem 2rem;
min-height: var(--stack-body-h);
box-shadow: 0 10px 32px rgba(0,0,0,0.25);
z-index: 10;
}
.f0 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 0); margin-left: calc(var(--tab-offset) * 0); background: #c9a86c; color: #2a2824; }
.f1 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 1); margin-left: calc(var(--tab-offset) * 1); background: #a8c4d4; color: #1a2830; }
.f2 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 2); margin-left: calc(var(--tab-offset) * 2); background: #b8d4a8; color: #1a2818; }
.f3 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 3); margin-left: calc(var(--tab-offset) * 3); background: #d4b8c4; color: #2a1820; }
.f4 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 4); margin-left: calc(var(--tab-offset) * 4); background: #d4c8a8; color: #2a2418; }
.f5 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 5); margin-left: calc(var(--tab-offset) * 5); background: #c4c4c4; color: #2a2a2a; }
.f6 .tab { top: calc(var(--stack-stick) + var(--stack-step) * 6); margin-left: calc(var(--tab-offset) * 6); background: #2a4a6b; color: #e8e2d4; }
.f0 { top: calc(var(--stack-stick) + var(--stack-step) * 0); }
.f0 .tab { margin-left: calc(var(--tab-offset) * 0); background: #c9a86c; color: #2a2824; }
.f0 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-tab-h)); }
.f1 { top: calc(var(--stack-stick) + var(--stack-step) * 1); }
.f1 .tab { margin-left: calc(var(--tab-offset) * 1); background: #a8c4d4; color: #1a2830; }
.f1 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 1 - var(--stack-tab-h)); }
.f2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
.f2 .tab { margin-left: calc(var(--tab-offset) * 2); background: #b8d4a8; color: #1a2818; }
.f2 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 2 - var(--stack-tab-h)); }
.f3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
.f3 .tab { margin-left: calc(var(--tab-offset) * 3); background: #d4b8c4; color: #2a1820; }
.f3 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 3 - var(--stack-tab-h)); }
.f4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); }
.f4 .tab { margin-left: calc(var(--tab-offset) * 4); background: #d4c8a8; color: #2a2418; }
.f4 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 4 - var(--stack-tab-h)); }
.f5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
.f5 .tab { margin-left: calc(var(--tab-offset) * 5); background: #c4c4c4; color: #2a2a2a; }
.f5 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 5 - var(--stack-tab-h)); }
.f6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); }
.f6 .tab { margin-left: calc(var(--tab-offset) * 6); background: #2a4a6b; color: #e8e2d4; }
.f6 .body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 6 - var(--stack-tab-h)); }
.body h1 { font-size: 1.65rem; margin-bottom: 0.35rem; }
.body h2 { font-size: 1.25rem; margin-bottom: 0.4rem; }

View File

@ -37,17 +37,16 @@ body {
.mount { padding: 0 0.25rem; }
.unit {
position: relative;
position: sticky;
margin: 0;
border: 1px solid #2a3448;
background: #161a22;
border-radius: 2px;
box-shadow: 0 6px 0 #0a0c10, 0 12px 24px rgba(0,0,0,0.4);
z-index: 10;
}
.unit-head {
position: sticky;
z-index: 60;
display: flex; align-items: center; gap: 0.5rem;
padding: 0.4rem 0.65rem;
background: #1a2030;
@ -72,26 +71,36 @@ body {
.unit-head a.ext { color: #60a5fa; text-decoration: none; font-size: 0.58rem; }
.unit-body {
position: sticky;
top: var(--stack-reveal);
padding: 0.6rem 0.7rem 2rem;
min-height: var(--stack-body-h);
z-index: 10;
background: #161a22;
}
.u0 { top: calc(var(--stack-stick) + var(--stack-step) * 0); }
.u0 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-tab-h)); }
.u1 { top: calc(var(--stack-stick) + var(--stack-step) * 1); }
.u1 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 1 - var(--stack-tab-h)); }
.u2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
.u2 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 2 - var(--stack-tab-h)); }
.u3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
.u3 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 3 - var(--stack-tab-h)); }
.u4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); }
.u4 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 4 - var(--stack-tab-h)); }
.u5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
.u5 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 5 - var(--stack-tab-h)); }
.u6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); }
.u6 .unit-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 6 - var(--stack-tab-h)); }
.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; }
.u0 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 0); }
.u1 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 1); }
.u2 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
.u3 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
.u4 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 4); }
.u5 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
.u6 .unit-head { top: calc(var(--stack-stick) + var(--stack-step) * 6); }
.foot {
display: flex; justify-content: space-between;
width: min(600px, 100%); margin: 0 auto;

View File

@ -27,27 +27,12 @@ body {
}
.frame {
position: relative;
position: sticky;
margin: 0 0.5rem;
border-left: 3px solid #3a3a44;
background: #141418;
box-shadow: 0 8px 0 #0a0a0c, 0 14px 28px rgba(0,0,0,0.45);
}
.frame-line {
position: sticky;
z-index: 60;
padding: 0.65rem 0 0.35rem 1rem;
background: #141418;
}
.frame-body {
position: sticky;
top: var(--stack-reveal);
padding: 0 0 2rem 1rem;
min-height: var(--stack-body-h);
z-index: 10;
background: #141418;
}
.frame-line {
@ -59,25 +44,43 @@ body {
cursor: pointer;
text-align: left;
width: 100%;
padding: 0.65rem 0 0.35rem 1rem;
background: #141418;
}
.frame-line:hover { color: #9fdf9f; text-decoration: underline; }
.frame-body {
padding: 0 0 2rem 1rem;
min-height: var(--stack-body-h);
background: #141418;
}
.f0 { top: calc(var(--stack-stick) + var(--stack-step) * 0); border-color: #c4a574; }
.f0 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-tab-h)); }
.f1 { top: calc(var(--stack-stick) + var(--stack-step) * 1); }
.f1 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 1 - var(--stack-tab-h)); }
.f2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
.f2 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 2 - var(--stack-tab-h)); }
.f3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
.f3 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 3 - var(--stack-tab-h)); }
.f4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); border-color: #6b8b9b; }
.f4 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 4 - var(--stack-tab-h)); }
.f5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
.f5 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 5 - var(--stack-tab-h)); }
.f6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); border-color: #7eb87a; }
.f6 .frame-body { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 6 - var(--stack-tab-h)); }
.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; }
.f0 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 0); }
.f0 { border-color: #c4a574; }
.f1 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 1); }
.f2 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
.f3 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
.f4 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 4); }
.f4 { border-color: #6b8b9b; }
.f5 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
.f6 .frame-line { top: calc(var(--stack-stick) + var(--stack-step) * 6); }
.f6 { border-color: #7eb87a; }
.foot {
display: flex; justify-content: space-between;
width: min(600px, 100%); margin: 0 auto;

View File

@ -4,6 +4,7 @@
:root {
--mono: 'IBM Plex Mono', ui-monospace, monospace;
--sans: 'Instrument Sans', system-ui, sans-serif;
--card-tab-h: 0.5rem;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
@ -35,26 +36,17 @@ body {
}
.layer {
position: relative;
position: sticky;
margin: 0;
border-radius: 8px 8px 6px 6px;
border: 1px solid rgba(255,255,255,0.08);
box-shadow: 0 12px 36px rgba(0,0,0,0.5);
}
.layer-inner {
position: sticky;
top: var(--stack-reveal);
z-index: 10;
}
.layer-tab {
position: sticky;
z-index: 55;
left: 10px;
right: 10px;
height: 6px;
margin-bottom: -6px;
margin: 0 10px -6px;
border-radius: 5px 5px 0 0;
background: inherit;
filter: brightness(1.12);
@ -63,26 +55,32 @@ body {
pointer-events: none;
}
.layer-0 { background: #1c1c20; }
.layer-0 .layer-tab { top: calc(var(--stack-stick) + var(--stack-step) * 0); }
.layer-1 { background: #24242c; }
.layer-1 .layer-tab { top: calc(var(--stack-stick) + var(--stack-step) * 1); }
.layer-2 { background: #2c2c36; }
.layer-2 .layer-tab { top: calc(var(--stack-stick) + var(--stack-step) * 2); }
.layer-3 { background: #343440; }
.layer-3 .layer-tab { top: calc(var(--stack-stick) + var(--stack-step) * 3); }
.layer-4 { background: #3c3c4a; }
.layer-4 .layer-tab { top: calc(var(--stack-stick) + var(--stack-step) * 4); }
.layer-5 { background: #444454; }
.layer-5 .layer-tab { top: calc(var(--stack-stick) + var(--stack-step) * 5); }
.layer-6 { background: #4c4c5e; }
.layer-6 .layer-tab { top: calc(var(--stack-stick) + var(--stack-step) * 6); }
.layer-inner {
padding: 1.2rem 1.35rem 2rem;
min-height: var(--stack-body-h);
}
.layer-0 { background: #1c1c20; top: calc(var(--stack-stick) + var(--stack-step) * 0); }
.layer-0 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--card-tab-h)); }
.layer-1 { background: #24242c; top: calc(var(--stack-stick) + var(--stack-step) * 1); }
.layer-1 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 1 - var(--card-tab-h)); }
.layer-2 { background: #2c2c36; top: calc(var(--stack-stick) + var(--stack-step) * 2); }
.layer-2 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 2 - var(--card-tab-h)); }
.layer-3 { background: #343440; top: calc(var(--stack-stick) + var(--stack-step) * 3); }
.layer-3 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 3 - var(--card-tab-h)); }
.layer-4 { background: #3c3c4a; top: calc(var(--stack-stick) + var(--stack-step) * 4); }
.layer-4 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 4 - var(--card-tab-h)); }
.layer-5 { background: #444454; top: calc(var(--stack-stick) + var(--stack-step) * 5); }
.layer-5 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 5 - var(--card-tab-h)); }
.layer-6 { background: #4c4c5e; top: calc(var(--stack-stick) + var(--stack-step) * 6); }
.layer-6 .layer-inner { margin-top: calc(var(--stack-reveal) - var(--stack-stick) - var(--stack-step) * 6 - var(--card-tab-h)); }
.layer-head {
display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.65rem;
margin-bottom: 0.65rem; padding-bottom: 0.5rem;