Fix folder stack overlap: pull slots, split tab/body sticky.

Sections overlap via negative margin; tabs stay in staggered rail;
active body z-index covers layers below as you scroll.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ilia 2026-05-20 23:05:20 -04:00
parent acfc6ddd86
commit bb8469ca10
10 changed files with 720 additions and 246 deletions

View File

@ -1,12 +1,16 @@
/* Sticky stack slots — import after stack-vars.css */
/* Pull layers into one stack so sticky overlap works */
.scroll-section {
height: var(--stack-slot);
position: relative;
}
.scroll-section + .scroll-section {
margin-top: calc(-1 * var(--stack-pull));
}
.scroll-section--final {
height: var(--stack-slot-last);
min-height: 280px;
min-height: 260px;
}
.stack-stop,

View File

@ -5,12 +5,13 @@ export function initStackScroll(options = {}) {
depthEl = document.getElementById('depth'),
depthPrefix = 'L',
tabSelector = '[data-goto], .jump',
bodySelector = '.body, .layer-inner, .frame-body, .unit-body',
} = options;
const sections = document.querySelectorAll(sectionSelector);
if (!sections.length) return;
const mid = () => window.innerHeight * 0.4;
const mid = () => window.innerHeight * 0.42;
function updateDepth() {
let active = 0;
@ -19,11 +20,19 @@ export function initStackScroll(options = {}) {
if (r.top <= mid() && r.bottom > mid()) active = Number(sec.dataset.layer);
});
if (depthEl) depthEl.textContent = `${depthPrefix}${active}`;
document.querySelectorAll('.stack-ruler button, .stack-ruler [data-goto], .tab-rail button, .tab[data-goto]').forEach((el) => {
const n = el.dataset.layer ?? el.dataset.goto;
if (n === undefined) return;
el.classList.toggle('active', Number(n) === active);
});
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;
});
}
document.querySelectorAll(tabSelector).forEach((tab) => {
@ -32,7 +41,7 @@ export function initStackScroll(options = {}) {
const layer = tab.dataset.goto;
const target = document.querySelector(`${sectionSelector}[data-layer="${layer}"]`);
if (!target) return;
const y = target.offsetTop - 48;
const y = target.getBoundingClientRect().top + window.scrollY - 48;
window.scrollTo({ top: Math.max(0, y), behavior: 'smooth' });
});
});

View File

@ -1,8 +1,13 @@
/* Shared stack scroll — one viewport per layer */
/* Shared stack scroll — overlapping sticky layers */
:root {
--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-reveal: calc(var(--stack-stick) + var(--stack-step) * 6 + var(--stack-tab-h));
--stack-slot: 100vh;
--stack-slot-last: 55vh;
--stack-slot-last: 50vh;
--stack-pull: calc(var(--stack-slot) - var(--stack-reveal));
--stack-body-h: calc(100dvh - var(--stack-reveal) - 1rem);
}

View File

@ -9,12 +9,24 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Literata:opsz,wght@7..72,400;7..72,600&display=swap" rel="stylesheet" />
<script>
(function () {
try {
var p = JSON.parse(localStorage.getItem('levkin-spec-prefs') || '{}');
if (p.theme) document.documentElement.dataset.theme = p.theme;
if (p.fontScale) document.documentElement.style.setProperty('--font-scale', p.fontScale);
} catch (e) { /* ignore */ }
})();
</script>
<link rel="stylesheet" href="./spec.css" />
</head>
<body>
<aside class="toc" aria-label="Table of contents">
<a class="skip-link" href="#main">Skip to specification</a>
<aside class="toc" aria-labelledby="toc-heading">
<a href="/" class="back">← all options</a>
<nav>
<h2 id="toc-heading" class="toc-title">Contents</h2>
<nav aria-label="Specification sections">
<a href="#abstract">Abstract</a>
<a href="#scope">1. Scope</a>
<a href="#services">2. Services</a>
@ -23,164 +35,208 @@
<a href="#subsidiaries">5. Subdomains</a>
<a href="#contact">6. Contact</a>
</nav>
<section class="prefs" aria-labelledby="prefs-heading">
<h2 id="prefs-heading" class="prefs-heading">Display</h2>
<fieldset class="theme-field">
<legend class="theme-legend">Theme</legend>
<div class="theme-options" role="radiogroup" aria-label="Color theme">
<label class="theme-option">
<input type="radio" name="theme" value="light" checked />
<span>Light</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="dim" />
<span>Dim</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="dark" />
<span>Dark</span>
</label>
</div>
</fieldset>
<div class="font-controls">
<span class="font-label" id="font-controls-label">Text size</span>
<div class="font-buttons" role="group" aria-labelledby="font-controls-label">
<button type="button" class="font-btn" data-action="decrease" aria-label="Decrease text size">A</button>
<button type="button" class="font-btn" data-action="reset" aria-label="Reset text size to default">A</button>
<button type="button" class="font-btn" data-action="increase" aria-label="Increase text size">A+</button>
</div>
<span class="font-scale-readout" id="font-scale-readout" aria-live="polite">100%</span>
</div>
</section>
<p class="meta">Levkin-Company-Spec<br />Version 1.0<br />May 2026</p>
</aside>
<article class="rfc">
<header class="rfc-header">
<p class="category">Request for Comments</p>
<h1>Levkin Software Development Company</h1>
<table class="rfc-meta">
<tr><th>Status</th><td><span class="badge">ACTIVE</span></td></tr>
<tr><th>Entity</th><td>Levkin</td></tr>
<tr><th>Domain</th><td>levkin.ca</td></tr>
<tr><th>Updated</th><td>2026-05-20</td></tr>
</table>
</header>
<main id="main" class="main" tabindex="-1">
<article class="rfc">
<header class="rfc-header">
<p class="category">Request for Comments</p>
<h1>Levkin Software Development Company</h1>
<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">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>
</table>
</header>
<section id="abstract">
<h2>Abstract</h2>
<p>This document describes <strong>Levkin</strong>, a Canadian software development practice specializing in production systems, business automation, and enterprise tooling. Remote across North American and European time zones. Levkin ships software that must work when nobody is watching — with error handling, documentation, and tests as non-optional requirements.</p>
<p>Quality engineering and SDET work are documented at <a href="https://iliadobkin.com">iliadobkin.com</a> — an interactive portfolio (Playwright-style test runner UI, career timeline, trace-driven debugging showcase).</p>
<p><span class="badge">AVAILABLE</span> Currently taking on new engagements.</p>
</section>
<section id="abstract" aria-labelledby="abstract-heading">
<h2 id="abstract-heading">Abstract</h2>
<p>This document describes <strong>Levkin</strong>, a Canadian software development practice specializing in production systems, business automation, and enterprise tooling. Remote across North American and European time zones. Levkin ships software that must work when nobody is watching — with error handling, documentation, and tests as non-optional requirements.</p>
<p>Quality engineering and SDET work are documented at <a href="https://iliadobkin.com" rel="noopener noreferrer">iliadobkin.com<span class="sr-only"> (external)</span></a> — an interactive portfolio (Playwright-style test runner UI, career timeline, trace-driven debugging showcase).</p>
<p><span class="badge">AVAILABLE</span> Currently taking on new engagements.</p>
</section>
<section id="scope">
<h2><span class="sec-num">1.</span> Scope</h2>
<p>Levkin operates as a boutique engineering practice, not a body shop. Engagements are scoped, shipped, and handed off with the expectation that the client (or their next hire) can maintain what was built.</p>
<pre class="code-block"><code>interface Engagement {
<section id="scope" aria-labelledby="scope-heading">
<h2 id="scope-heading"><span class="sec-num" aria-hidden="true">1.</span> Scope</h2>
<p>Levkin operates as a boutique engineering practice, not a body shop. Engagements are scoped, shipped, and handed off with the expectation that the client (or their next hire) can maintain what was built.</p>
<pre class="code-block" tabindex="0"><code>interface Engagement {
discovery: "15min call, no obligation"; // cal.levkin.ca/ilia/consult
delivery: "production-ready, documented";
maintenance: "optional, not mandatory lock-in";
}</code></pre>
</section>
</section>
<section id="services">
<h2><span class="sec-num">2.</span> Services</h2>
<p>The following endpoints represent primary service offerings. Each maps to a deployable capability.</p>
<section id="services" aria-labelledby="services-heading">
<h2 id="services-heading"><span class="sec-num" aria-hidden="true">2.</span> Services</h2>
<p>The following endpoints represent primary service offerings. Each maps to a deployable capability.</p>
<div class="endpoint">
<div class="endpoint-head">
<span class="method">BUILD</span>
<span class="path">/custom-software</span>
<article class="endpoint" aria-labelledby="ep-custom">
<div class="endpoint-head" id="ep-custom">
<span class="method" aria-hidden="true">BUILD</span>
<span class="path">/custom-software</span>
</div>
<p>Web applications, APIs, internal tools. Stack-agnostic; preference for boring, proven technology.</p>
<ul>
<li>TypeScript / Node · Python · C# / .NET</li>
<li>PostgreSQL · SQL Server · SQLite</li>
<li>React · Vue · server-rendered where appropriate</li>
</ul>
</article>
<article class="endpoint" aria-labelledby="ep-auto">
<div class="endpoint-head" id="ep-auto">
<span class="method" aria-hidden="true">BUILD</span>
<span class="path">/automation</span>
<span class="ext"><a href="https://auto.levkin.ca" rel="noopener noreferrer">auto.levkin.ca<span class="sr-only"> (external)</span></a></span>
</div>
<p>Production-ready automation — scripts, no-code workflows, CI/CD, webhooks, AI integrations. Runs while you sleep.</p>
<ul>
<li>n8n · Zapier · Make · GitHub Actions · Jenkins · Azure DevOps</li>
<li>Python · Node · Bash · macOS/iOS Shortcuts</li>
<li>OpenAI · Claude · custom LLM pipelines</li>
</ul>
</article>
<article class="endpoint" aria-labelledby="ep-caseware">
<div class="endpoint-head" id="ep-caseware">
<span class="method" aria-hidden="true">BUILD</span>
<span class="path">/caseware</span>
<span class="ext"><a href="https://caseware.levkin.ca" rel="noopener noreferrer">caseware.levkin.ca<span class="sr-only"> (external)</span></a></span>
</div>
<p>CaseWare &amp; CaseView features, client templates, release automation. 15+ years; teams at CaseWare International, MNP, JazzIt.</p>
<ul>
<li>C# · .NET · SQL Server · JavaScript automation</li>
<li>Template delivery · CI/CD · mentorship · modernization</li>
</ul>
</article>
<article class="endpoint" aria-labelledby="ep-qe">
<div class="endpoint-head" id="ep-qe">
<span class="method" aria-hidden="true">BUILD</span>
<span class="path">/quality-engineering</span>
<span class="ext"><a href="https://iliadobkin.com" rel="noopener noreferrer">iliadobkin.com<span class="sr-only"> (external)</span></a></span>
</div>
<p>Senior SDET services — test automation, CI/CD pipelines, trace-driven debugging, contract QA leadership.</p>
<ul>
<li>Portfolio: <a href="https://iliadobkin.com" rel="noopener noreferrer">iliadobkin.com<span class="sr-only"> (external)</span></a> — runnable career specs, trace viewer</li>
<li>Remote (ET) · Canadian · git.levkin.ca for source</li>
<li>Playwright / JS automation · enterprise CI (Jenkins, Azure DevOps, GitHub Actions)</li>
</ul>
</article>
</section>
<section id="proof" aria-labelledby="proof-heading">
<h2 id="proof-heading"><span class="sec-num" aria-hidden="true">3.</span> Documented Outcomes</h2>
<div class="table-wrap">
<table class="spec-table">
<caption class="sr-only">Documented outcomes metrics</caption>
<thead>
<tr><th scope="col">Metric</th><th scope="col">Value</th><th scope="col">Context</th></tr>
</thead>
<tbody>
<tr><td><code>release_time</code></td><td>8h → &lt;2min</td><td>CaseWare template pipeline rebuild</td></tr>
<tr><td><code>experience</code></td><td>15+ years</td><td>CaseWare, automation, enterprise CI/CD</td></tr>
<tr><td><code>automation_uptime</code></td><td>24/7</td><td>Pipelines monitored, not happy-path demos</td></tr>
<tr><td><code>engagement_model</code></td><td>Fixed scope</td><td>Quoted per project after discovery — no hourly surprises</td></tr>
</tbody>
</table>
</div>
<p>Web applications, APIs, internal tools. Stack-agnostic; preference for boring, proven technology.</p>
<ul>
<li>TypeScript / Node · Python · C# / .NET</li>
<li>PostgreSQL · SQL Server · SQLite</li>
<li>React · Vue · server-rendered where appropriate</li>
</ul>
</div>
<p>Engagement flow: <strong>Discover</strong> (15 min) → <strong>Design</strong> (proposal) → <strong>Ship</strong> (tested, documented) → <strong>Maintain</strong> (optional).</p>
</section>
<div class="endpoint">
<div class="endpoint-head">
<span class="method">BUILD</span>
<span class="path">/automation</span>
<span class="ext"><a href="https://auto.levkin.ca">auto.levkin.ca</a></span>
<section id="properties" aria-labelledby="properties-heading">
<h2 id="properties-heading"><span class="sec-num" aria-hidden="true">4.</span> Required Properties</h2>
<p>All Levkin deliverables MUST satisfy the following constraints unless explicitly waived in writing.</p>
<div class="table-wrap">
<table class="spec-table">
<caption class="sr-only">Required deliverable properties</caption>
<thead>
<tr><th scope="col">Property</th><th scope="col">Requirement</th><th scope="col">Rationale</th></tr>
</thead>
<tbody>
<tr><td><code>reliability</code></td><td>Retries, alerts, graceful degradation</td><td>Production ≠ demo</td></tr>
<tr><td><code>documentation</code></td><td>Runbook or README sufficient for handoff</td><td>Bus factor &gt; 1</td></tr>
<tr><td><code>testability</code></td><td>Automated tests before live data</td><td>Regressions are expensive</td></tr>
<tr><td><code>pragmatism</code></td><td>Smallest solution that solves the problem</td><td>20-line script &gt; 200-node workflow</td></tr>
</tbody>
</table>
</div>
<p>Production-ready automation — scripts, no-code workflows, CI/CD, webhooks, AI integrations. Runs while you sleep.</p>
<ul>
<li>n8n · Zapier · Make · GitHub Actions · Jenkins · Azure DevOps</li>
<li>Python · Node · Bash · macOS/iOS Shortcuts</li>
<li>OpenAI · Claude · custom LLM pipelines</li>
</ul>
</div>
</section>
<div class="endpoint">
<div class="endpoint-head">
<span class="method">BUILD</span>
<span class="path">/caseware</span>
<span class="ext"><a href="https://caseware.levkin.ca">caseware.levkin.ca</a></span>
<section id="subsidiaries" aria-labelledby="subsidiaries-heading">
<h2 id="subsidiaries-heading"><span class="sec-num" aria-hidden="true">5.</span> Registered Subdomains</h2>
<div class="table-wrap">
<table class="spec-table subdomains">
<caption class="sr-only">Registered Levkin subdomains</caption>
<thead>
<tr><th scope="col">Host</th><th scope="col">Purpose</th><th scope="col">Status</th></tr>
</thead>
<tbody>
<tr><td><a href="https://auto.levkin.ca" rel="noopener noreferrer">auto.levkin.ca<span class="sr-only"> (external)</span></a></td><td>Business automation</td><td><span class="badge">live</span></td></tr>
<tr><td><a href="https://caseware.levkin.ca" rel="noopener noreferrer">caseware.levkin.ca<span class="sr-only"> (external)</span></a></td><td>CaseWare consulting</td><td><span class="badge">live</span></td></tr>
<tr><td><a href="https://jobs.levkin.ca" rel="noopener noreferrer">jobs.levkin.ca<span class="sr-only"> (external)</span></a></td><td>Job orchestration (internal)</td><td><span class="badge muted">auth</span></td></tr>
<tr><td><a href="https://git.levkin.ca" rel="noopener noreferrer">git.levkin.ca<span class="sr-only"> (external)</span></a></td><td>Source control</td><td><span class="badge">live</span></td></tr>
<tr><td><a href="https://iliadobkin.com" rel="noopener noreferrer">iliadobkin.com<span class="sr-only"> (external)</span></a></td><td>SDET portfolio · quality engineering</td><td><span class="badge">live</span></td></tr>
<tr><td><a href="https://cal.levkin.ca/ilia/consult" rel="noopener noreferrer">cal.levkin.ca<span class="sr-only"> (external)</span></a></td><td>Scheduling · 15 min consultation</td><td><span class="badge">live</span></td></tr>
</tbody>
</table>
</div>
<p>CaseWare &amp; CaseView features, client templates, release automation. 15+ years; teams at CaseWare International, MNP, JazzIt.</p>
<ul>
<li>C# · .NET · SQL Server · JavaScript automation</li>
<li>Template delivery · CI/CD · mentorship · modernization</li>
</ul>
</div>
</section>
<div class="endpoint">
<div class="endpoint-head">
<span class="method">BUILD</span>
<span class="path">/quality-engineering</span>
<span class="ext"><a href="https://iliadobkin.com">iliadobkin.com</a></span>
<section id="contact" class="contact" aria-labelledby="contact-heading">
<h2 id="contact-heading"><span class="sec-num" aria-hidden="true">6.</span> Contact</h2>
<p>To initiate an engagement, send a <code>POST</code> to one of the following channels:</p>
<div class="contact-grid">
<a class="contact-card" href="https://cal.levkin.ca/ilia/consult" rel="noopener noreferrer">
<span class="method post" aria-hidden="true">POST</span>
<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">
<span class="method post" aria-hidden="true">POST</span>
<span class="contact-path">/email</span>
<span class="desc">hello@levkine.ca</span>
</a>
</div>
<p>Senior SDET services — test automation, CI/CD pipelines, trace-driven debugging, contract QA leadership.</p>
<ul>
<li>Portfolio: <a href="https://iliadobkin.com">iliadobkin.com</a> — runnable career specs, trace viewer</li>
<li>Remote (ET) · Canadian · git.levkin.ca for source</li>
<li>Playwright / JS automation · enterprise CI (Jenkins, Azure DevOps, GitHub Actions)</li>
</ul>
</div>
</section>
<section id="proof">
<h2><span class="sec-num">3.</span> Documented Outcomes</h2>
<table class="spec-table">
<thead>
<tr><th>Metric</th><th>Value</th><th>Context</th></tr>
</thead>
<tbody>
<tr><td><code>release_time</code></td><td>8h → &lt;2min</td><td>CaseWare template pipeline rebuild</td></tr>
<tr><td><code>experience</code></td><td>15+ years</td><td>CaseWare, automation, enterprise CI/CD</td></tr>
<tr><td><code>automation_uptime</code></td><td>24/7</td><td>Pipelines monitored, not happy-path demos</td></tr>
<tr><td><code>engagement_model</code></td><td>Fixed scope</td><td>Quoted per project after discovery — no hourly surprises</td></tr>
</tbody>
</table>
<p>Engagement flow: <strong>Discover</strong> (15 min) → <strong>Design</strong> (proposal) → <strong>Ship</strong> (tested, documented) → <strong>Maintain</strong> (optional).</p>
</section>
<section id="properties">
<h2><span class="sec-num">4.</span> Required Properties</h2>
<p>All Levkin deliverables MUST satisfy the following constraints unless explicitly waived in writing.</p>
<table class="spec-table">
<thead>
<tr><th>Property</th><th>Requirement</th><th>Rationale</th></tr>
</thead>
<tbody>
<tr><td><code>reliability</code></td><td>Retries, alerts, graceful degradation</td><td>Production ≠ demo</td></tr>
<tr><td><code>documentation</code></td><td>Runbook or README sufficient for handoff</td><td>Bus factor &gt; 1</td></tr>
<tr><td><code>testability</code></td><td>Automated tests before live data</td><td>Regressions are expensive</td></tr>
<tr><td><code>pragmatism</code></td><td>Smallest solution that solves the problem</td><td>20-line script &gt; 200-node workflow</td></tr>
</tbody>
</table>
</section>
<section id="subsidiaries">
<h2><span class="sec-num">5.</span> Registered Subdomains</h2>
<table class="spec-table subdomains">
<thead>
<tr><th>Host</th><th>Purpose</th><th>Status</th></tr>
</thead>
<tbody>
<tr><td><a href="https://auto.levkin.ca">auto.levkin.ca</a></td><td>Business automation</td><td><span class="badge">live</span></td></tr>
<tr><td><a href="https://caseware.levkin.ca">caseware.levkin.ca</a></td><td>CaseWare consulting</td><td><span class="badge">live</span></td></tr>
<tr><td><a href="https://jobs.levkin.ca">jobs.levkin.ca</a></td><td>Job orchestration (internal)</td><td><span class="badge muted">auth</span></td></tr>
<tr><td><a href="https://git.levkin.ca">git.levkin.ca</a></td><td>Source control</td><td><span class="badge">live</span></td></tr>
<tr><td><a href="https://iliadobkin.com">iliadobkin.com</a></td><td>SDET portfolio · quality engineering</td><td><span class="badge">live</span></td></tr>
<tr><td><a href="https://cal.levkin.ca/ilia/consult">cal.levkin.ca</a></td><td>Scheduling · 15 min consultation</td><td><span class="badge">live</span></td></tr>
</tbody>
</table>
</section>
<section id="contact" class="contact">
<h2><span class="sec-num">6.</span> Contact</h2>
<p>To initiate an engagement, send a <code>POST</code> to one of the following channels:</p>
<div class="contact-grid">
<a class="contact-card" href="https://cal.levkin.ca/ilia/consult">
<span class="method post">POST</span>
<span>/consult</span>
<span class="desc">15-minute discovery call</span>
</a>
<a class="contact-card" href="mailto:hello@levkine.ca?subject=Project%20enquiry">
<span class="method post">POST</span>
<span>/email</span>
<span class="desc">hello@levkine.ca</span>
</a>
</div>
<p class="copyright">© Levkin · Canadian software development</p>
</section>
</article>
<p class="copyright">© Levkin · Canadian software development</p>
</section>
</article>
</main>
<script src="./spec.js" type="module"></script>
</body>

View File

@ -1,19 +1,89 @@
:root {
/* --- themes --- */
:root,
[data-theme="light"] {
color-scheme: light;
--paper: #f4f1ec;
--surface: #ffffff;
--ink: #1a1a18;
--muted: #5c5a56;
--rule: #d4cfc6;
--accent: #2a4a6b;
--accent-bg: #e8eef4;
--muted: #4a4844;
--rule: #c8c2b8;
--accent: #1e4a72;
--accent-hover: #163a5c;
--accent-bg: #e4ecf4;
--link: #1e4a72;
--link-hover: #163a5c;
--badge-ok-bg: #c8e6ce;
--badge-ok-fg: #14532d;
--badge-post-bg: #f5dcc8;
--badge-post-fg: #6b3410;
--focus: #1e4a72;
--skip-bg: #1a1a18;
--skip-fg: #f4f1ec;
}
[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;
}
[data-theme="dark"] {
color-scheme: dark;
--paper: #141412;
--surface: #1e1e1c;
--ink: #ece9e4;
--muted: #b8b4ac;
--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;
}
:root {
--font-scale: 1;
--mono: 'IBM Plex Mono', ui-monospace, monospace;
--serif: 'Literata', Georgia, serif;
--focus-ring: 0 0 0 3px color-mix(in srgb, var(--focus) 35%, transparent);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html {
font-size: calc(100% * var(--font-scale));
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
}
body {
display: grid;
grid-template-columns: 200px 1fr;
grid-template-columns: minmax(12rem, 220px) 1fr;
min-height: 100vh;
background: var(--paper);
color: var(--ink);
@ -22,10 +92,64 @@ body {
line-height: 1.65;
}
/* screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.skip-link {
position: absolute;
top: 0.5rem;
left: 0.5rem;
z-index: 100;
padding: 0.6rem 1rem;
background: var(--skip-bg);
color: var(--skip-fg);
font-family: var(--mono);
font-size: 0.8rem;
text-decoration: none;
border-radius: 2px;
transform: translateY(-200%);
transition: transform 0.15s;
}
.skip-link:focus {
transform: translateY(0);
outline: 3px solid var(--focus);
outline-offset: 2px;
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
.code-block:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
a {
color: var(--link);
text-decoration-thickness: 1px;
text-underline-offset: 0.15em;
}
a:hover {
color: var(--link-hover);
}
@media (max-width: 800px) {
body { grid-template-columns: 1fr; }
.toc {
position: static !important;
height: auto !important;
border-bottom: 1px solid var(--rule);
padding: 1rem 1.5rem !important;
}
@ -37,40 +161,188 @@ body {
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
padding: 2rem 1.25rem;
border-right: 1px solid var(--rule);
font-family: var(--mono);
font-size: 0.7rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.back {
display: block;
color: var(--muted);
text-decoration: none;
margin-bottom: 1.5rem;
margin-bottom: 0.5rem;
min-height: 2.75rem;
line-height: 2.75rem;
}
.back:hover { color: var(--accent); }
.toc-title {
font-size: 0.65rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 0.25rem;
}
.toc nav {
display: flex;
flex-direction: column;
gap: 0.35rem;
gap: 0.15rem;
}
.toc nav a {
color: var(--ink);
text-decoration: none;
padding: 0.35rem 0.25rem;
min-height: 2.75rem;
display: flex;
align-items: center;
border-radius: 2px;
}
.toc nav a:hover { color: var(--accent); }
.toc nav a.is-active {
font-weight: 600;
color: var(--accent);
background: var(--accent-bg);
}
/* preferences */
.prefs {
margin-top: auto;
padding-top: 1rem;
border-top: 1px solid var(--rule);
}
.prefs-heading {
font-size: 0.65rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 0.6rem;
}
.theme-field {
border: 0;
margin: 0 0 0.75rem;
padding: 0;
}
.theme-legend {
font-size: 0.62rem;
color: var(--muted);
margin-bottom: 0.35rem;
}
.theme-options {
display: flex;
gap: 0.25rem;
}
.theme-option {
flex: 1;
cursor: pointer;
}
.theme-option input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.theme-option span {
display: flex;
align-items: center;
justify-content: center;
min-height: 2.75rem;
padding: 0.35rem 0.25rem;
border: 1px solid var(--rule);
background: var(--surface);
color: var(--ink);
text-align: center;
border-radius: 2px;
transition: background 0.12s, border-color 0.12s;
}
.theme-option input:checked + span {
border-color: var(--accent);
background: var(--accent-bg);
font-weight: 600;
color: var(--accent);
}
.theme-option input:focus-visible + span {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.font-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem 0.5rem;
}
.font-label {
width: 100%;
font-size: 0.62rem;
color: var(--muted);
}
.font-buttons {
display: flex;
gap: 0.25rem;
}
.font-btn {
font-family: var(--mono);
font-size: 0.75rem;
min-width: 2.75rem;
min-height: 2.75rem;
padding: 0.35rem 0.5rem;
border: 1px solid var(--rule);
background: var(--surface);
color: var(--ink);
cursor: pointer;
border-radius: 2px;
}
.font-btn:hover:not(:disabled) {
background: var(--accent-bg);
border-color: var(--accent);
}
.font-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.font-scale-readout {
font-size: 0.62rem;
color: var(--muted);
min-width: 2.5rem;
}
.meta {
margin-top: 2rem;
margin-top: 0.75rem;
color: var(--muted);
line-height: 1.5;
}
.main {
min-width: 0;
}
.rfc {
max-width: 42rem;
padding: 3rem 2.5rem 5rem;
@ -121,8 +393,8 @@ body {
font-family: var(--mono);
font-size: 0.65rem;
padding: 0.15rem 0.45rem;
background: #d4edda;
color: #1e5631;
background: var(--badge-ok-bg);
color: var(--badge-ok-fg);
letter-spacing: 0.04em;
}
@ -167,7 +439,7 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
.endpoint {
border: 1px solid var(--rule);
margin: 1.25rem 0;
background: #fff;
background: var(--surface);
}
.endpoint-head {
@ -184,8 +456,8 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
.method {
font-weight: 600;
color: #1e5631;
background: #d4edda;
color: var(--badge-ok-fg);
background: var(--badge-ok-bg);
padding: 0.15rem 0.4rem;
font-size: 0.7rem;
}
@ -194,6 +466,7 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
.ext { margin-left: auto; font-size: 0.75rem; }
.ext a { color: var(--muted); }
.ext a:hover { color: var(--link); }
.endpoint > p,
.endpoint > ul {
@ -208,11 +481,17 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
font-size: 0.85rem;
}
.table-wrap {
overflow-x: auto;
margin-top: 0.75rem;
-webkit-overflow-scrolling: touch;
}
.spec-table {
width: 100%;
min-width: 20rem;
border-collapse: collapse;
font-size: 0.9rem;
margin-top: 0.75rem;
}
.spec-table th,
@ -235,7 +514,7 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
color: var(--accent);
}
.spec-table a { color: var(--accent); }
.spec-table a { color: var(--link); }
.contact-grid {
display: grid;
@ -259,16 +538,18 @@ section p { margin-bottom: 0.75rem; color: var(--ink); }
font-family: var(--mono);
font-size: 0.85rem;
transition: background 0.15s;
min-height: 2.75rem;
}
.contact-card:hover {
.contact-card:hover,
.contact-card:focus-visible {
background: var(--accent-bg);
}
.method.post {
font-weight: 600;
color: #8b4513;
background: #fde8d8;
color: var(--badge-post-fg);
background: var(--badge-post-bg);
align-self: flex-start;
padding: 0.1rem 0.35rem;
font-size: 0.65rem;

View File

@ -1,17 +1,114 @@
const sections = document.querySelectorAll('section[id]');
const links = document.querySelectorAll('.toc nav a');
const STORAGE_KEY = 'levkin-spec-prefs';
const FONT_STEPS = [0.875, 1, 1.125, 1.25, 1.375, 1.5];
const DEFAULT_FONT_INDEX = 1;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
links.forEach((a) => {
a.style.fontWeight = a.getAttribute('href') === `#${entry.target.id}` ? '600' : '';
a.style.color = a.getAttribute('href') === `#${entry.target.id}` ? 'var(--accent)' : '';
});
function loadPrefs() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
} catch {
return {};
}
}
function savePrefs(prefs) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
}
function fontIndexFromScale(scale) {
const idx = FONT_STEPS.indexOf(scale);
return idx === -1 ? DEFAULT_FONT_INDEX : idx;
}
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme === 'dark' ? 'dark' : theme === 'dim' ? 'light' : 'light';
const input = document.querySelector(`input[name="theme"][value="${theme}"]`);
if (input) input.checked = true;
}
function applyFontScale(scale) {
document.documentElement.style.setProperty('--font-scale', String(scale));
const readout = document.getElementById('font-scale-readout');
if (readout) readout.textContent = `${Math.round(scale * 100)}%`;
document.querySelectorAll('.font-btn').forEach((btn) => {
const action = btn.dataset.action;
if (action === 'decrease') btn.disabled = scale <= FONT_STEPS[0];
if (action === 'increase') btn.disabled = scale >= FONT_STEPS[FONT_STEPS.length - 1];
});
}
function initPreferences() {
const prefs = loadPrefs();
const theme = prefs.theme || 'light';
const fontScale = prefs.fontScale || FONT_STEPS[DEFAULT_FONT_INDEX];
let fontIndex = fontIndexFromScale(fontScale);
applyTheme(theme);
applyFontScale(fontScale);
document.querySelectorAll('input[name="theme"]').forEach((input) => {
input.addEventListener('change', () => {
const next = { ...loadPrefs(), theme: input.value };
savePrefs(next);
applyTheme(input.value);
});
},
{ rootMargin: '-30% 0px -60% 0px' }
);
});
sections.forEach((s) => observer.observe(s));
document.querySelectorAll('.font-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const action = btn.dataset.action;
if (action === 'reset') fontIndex = DEFAULT_FONT_INDEX;
else if (action === 'decrease' && fontIndex > 0) fontIndex -= 1;
else if (action === 'increase' && fontIndex < FONT_STEPS.length - 1) fontIndex += 1;
const scale = FONT_STEPS[fontIndex];
const next = { ...loadPrefs(), fontScale: scale };
savePrefs(next);
applyFontScale(scale);
});
});
}
function initScrollSpy() {
const sections = document.querySelectorAll('section[id]');
const links = document.querySelectorAll('.toc nav a');
if (!sections.length || !links.length) return;
const setActive = (id) => {
links.forEach((a) => {
const active = a.getAttribute('href') === `#${id}`;
a.classList.toggle('is-active', active);
if (active) a.setAttribute('aria-current', 'location');
else a.removeAttribute('aria-current');
});
};
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
const onScroll = () => {
let current = sections[0]?.id;
const offset = 120;
sections.forEach((section) => {
if (section.getBoundingClientRect().top - offset <= 0) current = section.id;
});
if (current) setActive(current);
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
setActive(entry.target.id);
});
},
{ rootMargin: '-30% 0px -60% 0px' }
);
sections.forEach((s) => observer.observe(s));
}
initPreferences();
initScrollSpy();

View File

@ -60,18 +60,14 @@ body { font-family: var(--sans); background: #2a2824; color: #1a1814; }
padding: var(--stack-nav) 1rem 0;
}
/* One sticky unit: tab + body move together */
.folder {
position: sticky;
position: relative;
width: 100%;
margin: 0;
padding: 0;
background: transparent;
}
/* Tab on top — NOT sticky alone; offset per layer */
/* Tabs stick in staggered rail — always above folder bodies */
.tab {
position: relative;
position: sticky;
display: block;
width: fit-content;
max-width: calc(100% - 1rem);
@ -87,41 +83,31 @@ 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: calc(100vh - var(--stack-nav) - 5rem);
box-shadow: 0 10px 32px rgba(0,0,0,0.2);
min-height: var(--stack-body-h);
box-shadow: 0 10px 32px rgba(0,0,0,0.25);
z-index: 10;
}
/* Sticky top steps — tabs stay visible above lower folders */
.f0 { top: calc(var(--stack-stick) + var(--stack-step) * 0); z-index: 10; }
.f0 .tab { margin-left: calc(var(--tab-offset) * 0); background: #c9a86c; color: #2a2824; }
.f1 { top: calc(var(--stack-stick) + var(--stack-step) * 1); z-index: 11; }
.f1 .tab { margin-left: calc(var(--tab-offset) * 1); background: #a8c4d4; color: #1a2830; }
.f2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); z-index: 12; }
.f2 .tab { margin-left: calc(var(--tab-offset) * 2); background: #b8d4a8; color: #1a2818; }
.f3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); z-index: 13; }
.f3 .tab { margin-left: calc(var(--tab-offset) * 3); background: #d4b8c4; color: #2a1820; }
.f4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); z-index: 14; }
.f4 .tab { margin-left: calc(var(--tab-offset) * 4); background: #d4c8a8; color: #2a2418; }
.f5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); z-index: 15; }
.f5 .tab { margin-left: calc(var(--tab-offset) * 5); background: #c4c4c4; color: #2a2a2a; }
.f6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); z-index: 16; }
.f6 .tab { margin-left: calc(var(--tab-offset) * 6); background: #2a4a6b; color: #e8e2d4; }
.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; }
.body h1 { font-size: 1.65rem; margin-bottom: 0.35rem; }
.body h2 { font-size: 1.25rem; margin-bottom: 0.4rem; }

View File

@ -37,16 +37,17 @@ body {
.mount { padding: 0 0.25rem; }
.unit {
position: sticky;
position: relative;
margin: 0;
border: 1px solid #2a3448;
background: #161a22;
border-radius: 2px;
overflow: hidden;
box-shadow: 0 6px 0 #0a0c10, 0 12px 24px rgba(0,0,0,0.4);
}
.unit-head {
position: sticky;
z-index: 60;
display: flex; align-items: center; gap: 0.5rem;
padding: 0.4rem 0.65rem;
background: #1a2030;
@ -71,21 +72,25 @@ 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: calc(100vh - var(--stack-nav) - 5rem);
min-height: var(--stack-body-h);
z-index: 10;
background: #161a22;
}
.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 { top: calc(var(--stack-stick) + var(--stack-step) * 0); z-index: 10; }
.u1 { top: calc(var(--stack-stick) + var(--stack-step) * 1); z-index: 11; }
.u2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); z-index: 12; }
.u3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); z-index: 13; }
.u4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); z-index: 14; }
.u5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); z-index: 15; }
.u6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); z-index: 16; }
.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;

View File

@ -27,26 +27,37 @@ body {
}
.frame {
position: sticky;
position: relative;
margin: 0 0.5rem;
border-left: 3px solid #3a3a44;
background: #141418;
padding: 0.65rem 0 0.65rem 1rem;
min-height: calc(100vh - var(--stack-nav) - 5rem);
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 {
display: block;
font-size: 0.66rem;
color: #6b9b6b;
margin-bottom: 0.4rem;
background: none;
border: none;
font-family: inherit;
cursor: pointer;
text-align: left;
padding: 0;
width: 100%;
}
@ -56,13 +67,16 @@ body {
.frame-body p { color: #6b6966; font-size: 0.74rem; }
.frame-body a { color: #8b9cb3; text-decoration: none; }
.f0 { top: calc(var(--stack-stick) + var(--stack-step) * 0); z-index: 10; border-color: #c4a574; }
.f1 { top: calc(var(--stack-stick) + var(--stack-step) * 1); z-index: 11; }
.f2 { top: calc(var(--stack-stick) + var(--stack-step) * 2); z-index: 12; }
.f3 { top: calc(var(--stack-stick) + var(--stack-step) * 3); z-index: 13; }
.f4 { top: calc(var(--stack-stick) + var(--stack-step) * 4); z-index: 14; border-color: #6b8b9b; }
.f5 { top: calc(var(--stack-stick) + var(--stack-step) * 5); z-index: 15; }
.f6 { top: calc(var(--stack-stick) + var(--stack-step) * 6); z-index: 16; border-color: #7eb87a; }
.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;

View File

@ -35,16 +35,26 @@ body {
}
.layer {
position: sticky;
position: relative;
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: absolute;
top: -6px; left: 10px; right: 10px; height: 6px;
position: sticky;
z-index: 55;
left: 10px;
right: 10px;
height: 6px;
margin-bottom: -6px;
border-radius: 5px 5px 0 0;
background: inherit;
filter: brightness(1.12);
@ -53,17 +63,24 @@ body {
pointer-events: none;
}
.layer-0 { z-index: 10; background: #1c1c20; top: calc(var(--stack-stick) + var(--stack-step) * 0); }
.layer-1 { z-index: 11; background: #24242c; top: calc(var(--stack-stick) + var(--stack-step) * 1); }
.layer-2 { z-index: 12; background: #2c2c36; top: calc(var(--stack-stick) + var(--stack-step) * 2); }
.layer-3 { z-index: 13; background: #343440; top: calc(var(--stack-stick) + var(--stack-step) * 3); }
.layer-4 { z-index: 14; background: #3c3c4a; top: calc(var(--stack-stick) + var(--stack-step) * 4); }
.layer-5 { z-index: 15; background: #444454; top: calc(var(--stack-stick) + var(--stack-step) * 5); }
.layer-6 { z-index: 16; background: #4c4c5e; top: calc(var(--stack-stick) + var(--stack-step) * 6); }
.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: calc(100vh - var(--stack-nav) - 5rem);
min-height: var(--stack-body-h);
}
.layer-head {