diff --git a/package-lock.json b/package-lock.json index 3a7af74..d2d10ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1e28803..08c58e8 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "devDependencies": { + "playwright": "^1.60.0", "vite": "^6.3.5" } } diff --git a/scripts/debug-sticky.mjs b/scripts/debug-sticky.mjs new file mode 100644 index 0000000..9da2982 --- /dev/null +++ b/scripts/debug-sticky.mjs @@ -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(); diff --git a/scripts/test-stack.mjs b/scripts/test-stack.mjs new file mode 100644 index 0000000..8b1d80a --- /dev/null +++ b/scripts/test-stack.mjs @@ -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); diff --git a/shared/stack-layout.css b/shared/stack-layout.css index 0802803..49ef3cf 100644 --- a/shared/stack-layout.css +++ b/shared/stack-layout.css @@ -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; +} diff --git a/shared/stack-scroll.js b/shared/stack-scroll.js index da39179..5426fed 100644 --- a/shared/stack-scroll.js +++ b/shared/stack-scroll.js @@ -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; }); } diff --git a/shared/stack-vars.css b/shared/stack-vars.css index 566d4fa..6cebfc0 100644 --- a/shared/stack-vars.css +++ b/shared/stack-vars.css @@ -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 L0–L6 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); } diff --git a/spec/index.html b/spec/index.html index dc88e8e..ede0f08 100644 --- a/spec/index.html +++ b/spec/index.html @@ -77,7 +77,7 @@ - + @@ -227,13 +227,14 @@ /consult15-minute discovery call (opens cal.levkin.ca) - + /email - hello@levkine.ca + ilia@levkine.ca + ↑ Back to top diff --git a/spec/spec.css b/spec/spec.css index aecbc87..66dcaa6 100644 --- a/spec/spec.css +++ b/spec/spec.css @@ -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; + } +} diff --git a/spec/spec.js b/spec/spec.js index d9af9f2..5cc100b 100644 --- a/spec/spec.js +++ b/spec/spec.js @@ -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; } diff --git a/stack-folder/folder.css b/stack-folder/folder.css index 10a6f7a..4acae53 100644 --- a/stack-folder/folder.css +++ b/stack-folder/folder.css @@ -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; } diff --git a/stack-rack/rack.css b/stack-rack/rack.css index 2de8ba6..d298624 100644 --- a/stack-rack/rack.css +++ b/stack-rack/rack.css @@ -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; diff --git a/stack-trace/trace.css b/stack-trace/trace.css index 960da83..0768efd 100644 --- a/stack-trace/trace.css +++ b/stack-trace/trace.css @@ -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; diff --git a/stack/stack.css b/stack/stack.css index f359633..b6a718f 100644 --- a/stack/stack.css +++ b/stack/stack.css @@ -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;
StatusACTIVE
EntityLevkin
EntityLevkin Inc.
Domainlevkin.ca
Updated